Skip to content

Commit adf5b62

Browse files
authored
feat: Add Service Stub Pattern using Sentiment Analysis example (#3215)
* Add Service Stub Pattern using Sentiment Analysis example * Fix Checkstyle issues * Suppress Sonar warning for Random usage in RealSentimentAnalysisServer
1 parent abfb137 commit adf5b62

File tree

10 files changed

+377
-0
lines changed

10 files changed

+377
-0
lines changed

pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@
224224
<module>table-inheritance</module>
225225
<module>bloc</module>
226226
<module>map-reduce</module>
227+
<module>service-stub</module>
227228
</modules>
228229
<repositories>
229230
<repository>

service-stub/README.md

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
title: "Service Stub Pattern in Java: Simplifying Testing with Stub Implementations"
3+
shortTitle: Service Stub
4+
description: "Explore the Service Stub design pattern in Java using a Sentiment Analysis example. Learn how stub implementations provide dummy services to facilitate testing and development."
5+
category: Structural
6+
language: en
7+
tag:
8+
- Testing
9+
- Decoupling
10+
- Dummy Services
11+
- Dependency Injection
12+
---
13+
14+
## Also known as
15+
16+
* Dummy Service
17+
* Fake Service
18+
19+
## Intent of Service Stub Pattern
20+
21+
The Service Stub pattern provides a lightweight, dummy implementation of an external service to allow testing or development without relying on the real service, which may be unavailable, slow, or resource-intensive.
22+
23+
## Detailed Explanation of Service Stub Pattern with Real-World Example
24+
25+
Real-world example
26+
27+
> In this example, we simulate a **Sentiment Analysis Service**. The real implementation delays execution and randomly decides the sentiment. The stub implementation, on the other hand, quickly returns predefined responses based on input text ("good", "bad", or neutral), making it ideal for testing.
28+
29+
In plain words
30+
31+
> Use a fake service to return predictable results without relying on external systems.
32+
33+
Wikipedia says
34+
35+
> A test stub is a dummy component used during testing to isolate behavior.
36+
37+
## Programmatic Example of Service Stub Pattern in Java
38+
39+
We define a `SentimentAnalysisService` interface and provide two implementations:
40+
41+
1. **RealSentimentAnalysisServer**: Simulates a slow, random sentiment analysis system.
42+
2. **StubSentimentAnalysisServer**: Returns a deterministic result based on input keywords.
43+
44+
### Example Implementation
45+
Both the real service and the stub implement the interface below.
46+
```java
47+
public interface SentimentAnalysisServer {
48+
String analyzeSentiment(String text);
49+
}
50+
```
51+
The real sentiment analysis class returns a random response for a given input and simulates the runtime by sleeping
52+
the Thread for 5 seconds. The Supplier<Integer> allows injecting controlled sentiment values during testing, ensuring
53+
deterministic outputs.
54+
```java
55+
public class RealSentimentAnalysisServer implements SentimentAnalysisServer {
56+
57+
private final Supplier<Integer> sentimentSupplier;
58+
59+
public RealSentimentAnalysisServer(Supplier<Integer> sentimentSupplier) {
60+
this.sentimentSupplier = sentimentSupplier;
61+
}
62+
63+
public RealSentimentAnalysisServer() {
64+
this(() -> new Random().nextInt(3));
65+
}
66+
67+
@Override
68+
public String analyzeSentiment(String text) {
69+
int sentiment = sentimentSupplier.get();
70+
try {
71+
Thread.sleep(5000);
72+
} catch (InterruptedException e) {
73+
Thread.currentThread().interrupt();
74+
}
75+
return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral";
76+
}
77+
}
78+
```
79+
The stub implementation simulates the real sentiment analysis class and provides a deterministic output
80+
for a given input. Additionally, its runtime is almost zero.
81+
```java
82+
public class StubSentimentAnalysisServer implements SentimentAnalysisServer {
83+
84+
@Override
85+
public String analyzeSentiment(String text) {
86+
if (text.toLowerCase().contains("good")) {
87+
return "Positive";
88+
}
89+
else if (text.toLowerCase().contains("bad")) {
90+
return "Negative";
91+
}
92+
else {
93+
return "Neutral";
94+
}
95+
}
96+
}
97+
98+
```
99+
Here is the main function of the App class (entry point to the program)
100+
```java
101+
@Slf4j
102+
public static void main(String[] args) {
103+
LOGGER.info("Setting up the real sentiment analysis server.");
104+
RealSentimentAnalysisServer realSentimentAnalysisServer = new RealSentimentAnalysisServer();
105+
String text = "This movie is soso";
106+
LOGGER.info("Analyzing input: {}", text);
107+
String sentiment = realSentimentAnalysisServer.analyzeSentiment(text);
108+
LOGGER.info("The sentiment is: {}", sentiment);
109+
110+
LOGGER.info("Setting up the stub sentiment analysis server.");
111+
StubSentimentAnalysisServer stubSentimentAnalysisServer = new StubSentimentAnalysisServer();
112+
text = "This movie is so bad";
113+
LOGGER.info("Analyzing input: {}", text);
114+
sentiment = stubSentimentAnalysisServer.analyzeSentiment(text);
115+
LOGGER.info("The sentiment is: {}", sentiment);
116+
}
117+
```
118+
## When to Use the Service Stub Pattern in Java
119+
120+
Use the Service Stub pattern when:
121+
122+
* Testing components that depend on external services.
123+
* The real service is slow, unreliable, or unavailable.
124+
* You need predictable, predefined responses.
125+
* Developing offline without real service access.
126+
127+
## Real-World Applications of Service Stub Pattern in Java
128+
129+
* Simulating APIs (payments, recommendation systems) during testing.
130+
* Bypassing external AI/ML models in tests.
131+
* Simplifying integration testing.
132+
133+
## Benefits and Trade-offs of Service Stub Pattern
134+
135+
Benefits:
136+
137+
* Reduces dependencies.
138+
* Provides predictable behavior.
139+
* Speeds up testing.
140+
141+
Trade-offs:
142+
143+
* Requires maintaining stub logic.
144+
* May not fully represent real service behavior.
145+
146+
## Related Java Design Patterns
147+
148+
* [Proxy](https://java-design-patterns.com/patterns/proxy/)
149+
* [Strategy](https://java-design-patterns.com/patterns/strategy/)
150+
151+
## References and Credits
152+
153+
* [Martin Fowler: Test Stubs](https://martinfowler.com/articles/mocksArentStubs.html)

service-stub/pom.xml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.iluwatar</groupId>
8+
<artifactId>java-design-patterns</artifactId>
9+
<version>1.26.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>service-stub</artifactId>
13+
14+
<properties>
15+
<maven.compiler.source>17</maven.compiler.source>
16+
<maven.compiler.target>17</maven.compiler.target>
17+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
18+
</properties>
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.junit.jupiter</groupId>
22+
<artifactId>junit-jupiter</artifactId>
23+
<scope>test</scope>
24+
</dependency>
25+
</dependencies>
26+
27+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.iluwatar.servicestub;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
/**
6+
* A Service Stub is a dummy implementation of an external service used during development or
7+
* testing. The purpose is to provide a lightweight "stub" when the real service may not always be
8+
* available (or too slow to use during testing).
9+
*
10+
* <p>This implementation simulates a simple sentiment analysis program, where a text is analyzed to
11+
* deduce whether it is a positive, negative or neutral sentiment. The stub returns a response based
12+
* on whether the analyzed text contains the words "good" or "bad", not accounting for stopwords or
13+
* the underlying semantic of the text.
14+
*
15+
* <p>The "real" sentiment analysis class simulates the processing time for the request by pausing
16+
* the execution of the thread for 5 seconds. In the stub sentiment analysis class the response is
17+
* immediate. In addition, the stub returns a deterministic output with regard to the input. This
18+
* is extra useful for testing purposes.
19+
*/
20+
21+
22+
@Slf4j
23+
public class App {
24+
/**
25+
* Program entry point.
26+
*
27+
* @param args command line args
28+
*/
29+
public static void main(String[] args) {
30+
LOGGER.info("Setting up the real sentiment analysis server.");
31+
RealSentimentAnalysisServer realSentimentAnalysisServer = new RealSentimentAnalysisServer();
32+
String text = "This movie is soso";
33+
LOGGER.info("Analyzing input: {}", text);
34+
String sentiment = realSentimentAnalysisServer.analyzeSentiment(text);
35+
LOGGER.info("The sentiment is: {}", sentiment);
36+
37+
LOGGER.info("Setting up the stub sentiment analysis server.");
38+
StubSentimentAnalysisServer stubSentimentAnalysisServer = new StubSentimentAnalysisServer();
39+
text = "This movie is so bad";
40+
LOGGER.info("Analyzing input: {}", text);
41+
sentiment = stubSentimentAnalysisServer.analyzeSentiment(text);
42+
LOGGER.info("The sentiment is: {}", sentiment);
43+
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.iluwatar.servicestub;
2+
3+
import java.util.Random;
4+
import java.util.function.Supplier;
5+
6+
/**
7+
* Real implementation of SentimentAnalysisServer.
8+
* Simulates random sentiment classification with processing delay.
9+
*/
10+
11+
public class RealSentimentAnalysisServer implements SentimentAnalysisServer {
12+
/**
13+
* A real sentiment analysis implementation would analyze the input string using, e.g., NLP and
14+
* determine whether the sentiment is positive, negative or neutral. Here we simply choose a random
15+
* number to simulate this. The "model" may take some time to process the input and we simulate
16+
* this by delaying the execution 5 seconds.
17+
*
18+
* @param text the input string to analyze
19+
* @return sentiment classification result (Positive, Negative, or Neutral)
20+
*/
21+
22+
private final Supplier<Integer> sentimentSupplier;
23+
24+
// Constructor
25+
public RealSentimentAnalysisServer(Supplier<Integer> sentimentSupplier) {
26+
this.sentimentSupplier = sentimentSupplier;
27+
}
28+
29+
@SuppressWarnings("java:S2245") // Safe use: Randomness is for simulation/testing only
30+
public RealSentimentAnalysisServer() {
31+
this(() -> new Random().nextInt(3));
32+
}
33+
34+
@Override
35+
public String analyzeSentiment(String text) {
36+
int sentiment = sentimentSupplier.get();
37+
try {
38+
Thread.sleep(5000);
39+
} catch (InterruptedException e) {
40+
Thread.currentThread().interrupt();
41+
}
42+
return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral";
43+
}
44+
}
45+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.iluwatar.servicestub;
2+
3+
/**
4+
* Sentiment analysis server interface to be implemented by sentiment analysis services.
5+
*/
6+
7+
public interface SentimentAnalysisServer {
8+
/**
9+
* Analyzes the sentiment of the input text and returns the result.
10+
*
11+
* @param text the input text to analyze
12+
* @return sentiment classification result
13+
*/
14+
String analyzeSentiment(String text);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.iluwatar.servicestub;
2+
3+
/**
4+
* Stub implementation of SentimentAnalysisServer.
5+
* Returns deterministic sentiment based on input keywords.
6+
*/
7+
8+
public class StubSentimentAnalysisServer implements SentimentAnalysisServer {
9+
10+
/**
11+
* Fake sentiment analyzer, always returns "Positive" if input string contains the word "good",
12+
* "Negative" if the string contains "bad" and "Neutral" otherwise.
13+
*
14+
* @param text the input string to analyze
15+
* @return sentiment classification result (Positive, Negative, or Neutral)
16+
*/
17+
@Override
18+
public String analyzeSentiment(String text) {
19+
if (text.toLowerCase().contains("good")) {
20+
return "Positive";
21+
} else if (text.toLowerCase().contains("bad")) {
22+
return "Negative";
23+
} else {
24+
return "Neutral";
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.iluwatar.servicestub;
2+
3+
import org.junit.jupiter.api.Test;
4+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
5+
6+
public class AppTest {
7+
@Test
8+
void shouldExecuteWithoutException() {
9+
assertDoesNotThrow(() -> App.main(new String[] {}));
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.iluwatar.servicestub;
2+
3+
import org.junit.jupiter.api.Test;
4+
import static org.junit.jupiter.api.Assertions.*;
5+
6+
class RealSentimentAnalysisServerTest {
7+
8+
@Test
9+
void testPositiveSentiment() {
10+
RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 0);
11+
assertEquals("Positive", server.analyzeSentiment("Test"));
12+
}
13+
14+
@Test
15+
void testNegativeSentiment() {
16+
RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 1);
17+
assertEquals("Negative", server.analyzeSentiment("Test"));
18+
}
19+
20+
@Test
21+
void testNeutralSentiment() {
22+
RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 2);
23+
assertEquals("Neutral", server.analyzeSentiment("Test"));
24+
}
25+
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.iluwatar.servicestub;
2+
3+
import org.junit.jupiter.api.Test;
4+
import static org.junit.jupiter.api.Assertions.*;
5+
6+
class StubSentimentAnalysisServerTest {
7+
8+
private final StubSentimentAnalysisServer stub = new StubSentimentAnalysisServer();
9+
10+
@Test
11+
void testPositiveSentiment() {
12+
String result = stub.analyzeSentiment("This is a good product");
13+
assertEquals("Positive", result);
14+
}
15+
16+
@Test
17+
void testNegativeSentiment() {
18+
String result = stub.analyzeSentiment("This is a bad product");
19+
assertEquals("Negative", result);
20+
}
21+
22+
@Test
23+
void testNeutralSentiment() {
24+
String result = stub.analyzeSentiment("This product is average");
25+
assertEquals("Neutral", result);
26+
}
27+
}

0 commit comments

Comments
 (0)