Skip to content

Commit 7c11cba

Browse files
committed
Add Service Stub Pattern using Sentiment Analysis example
1 parent 894ae8f commit 7c11cba

File tree

10 files changed

+369
-0
lines changed

10 files changed

+369
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
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

Lines changed: 153 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
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>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.iluwatar.servicestub;
2+
import lombok.extern.slf4j.Slf4j;
3+
4+
/**
5+
* A Service Stub is a dummy implementation of an external service used during development or
6+
* testing. The purpose is to provide a lightweight "stub" when the real service may not always be
7+
* available (or too slow to use during testing).
8+
*
9+
* <p>This implementation simulates a simple sentiment analysis program, where a text is analyzed to
10+
* deduce whether it is a positive, negative or neutral sentiment. The stub returns a response based
11+
* on whether the analyzed text contains the words "good" or "bad", not accounting for stopwords or
12+
* the underlying semantic of the text.
13+
*
14+
* <p>The "real" sentiment analysis class simulates the processing time for the request by pausing
15+
* the execution of the thread for 5 seconds. In the stub sentiment analysis class the response is
16+
* immediate. In addition, the stub returns a deterministic output with regard to the input. This
17+
* is extra useful for testing purposes.
18+
*/
19+
20+
21+
@Slf4j
22+
public class App {
23+
/**
24+
* Program entry point.
25+
*
26+
* @param args command line args
27+
*/
28+
public static void main(String[] args) {
29+
LOGGER.info("Setting up the real sentiment analysis server.");
30+
RealSentimentAnalysisServer realSentimentAnalysisServer = new RealSentimentAnalysisServer();
31+
String text = "This movie is soso";
32+
LOGGER.info("Analyzing input: {}", text);
33+
String sentiment = realSentimentAnalysisServer.analyzeSentiment(text);
34+
LOGGER.info("The sentiment is: {}", sentiment);
35+
36+
LOGGER.info("Setting up the stub sentiment analysis server.");
37+
StubSentimentAnalysisServer stubSentimentAnalysisServer = new StubSentimentAnalysisServer();
38+
text = "This movie is so bad";
39+
LOGGER.info("Analyzing input: {}", text);
40+
sentiment = stubSentimentAnalysisServer.analyzeSentiment(text);
41+
LOGGER.info("The sentiment is: {}", sentiment);
42+
43+
}
44+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.iluwatar.servicestub;
2+
3+
import java.util.Random;
4+
import java.util.function.Supplier;
5+
6+
public class RealSentimentAnalysisServer implements SentimentAnalysisServer {
7+
/**
8+
* A real sentiment analysis implementation would analyze the input string using, e.g., NLP and
9+
* determine whether the sentiment is positive, negative or neutral. Here we simply choose a random
10+
* number to simulate this. The "model" may take some time to process the input and we simulate
11+
* this by delaying the execution 5 seconds.
12+
*
13+
* @param text the input string to analyze
14+
* @return sentiment classification result (Positive, Negative, or Neutral)
15+
*/
16+
17+
private final Supplier<Integer> sentimentSupplier;
18+
19+
// Constructor
20+
public RealSentimentAnalysisServer(Supplier<Integer> sentimentSupplier) {
21+
this.sentimentSupplier = sentimentSupplier;
22+
}
23+
24+
public RealSentimentAnalysisServer() {
25+
this(() -> new Random().nextInt(3));
26+
}
27+
28+
@Override
29+
public String analyzeSentiment(String text) {
30+
int sentiment = sentimentSupplier.get();
31+
try {
32+
Thread.sleep(5000);
33+
} catch (InterruptedException e) {
34+
Thread.currentThread().interrupt();
35+
}
36+
return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral";
37+
}
38+
}
39+
Lines changed: 15 additions & 0 deletions
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+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.iluwatar.servicestub;
2+
3+
public class StubSentimentAnalysisServer implements SentimentAnalysisServer {
4+
5+
/**
6+
* Fake sentiment analyzer, always returns "Positive" if input string contains the word "good",
7+
* "Negative" if the string contains "bad" and "Neutral" otherwise.
8+
*
9+
* @param text the input string to analyze
10+
* @return sentiment classification result (Positive, Negative, or Neutral)
11+
*/
12+
@Override
13+
public String analyzeSentiment(String text) {
14+
if (text.toLowerCase().contains("good")) {
15+
return "Positive";
16+
}
17+
else if (text.toLowerCase().contains("bad")) {
18+
return "Negative";
19+
}
20+
else {
21+
return "Neutral";
22+
}
23+
}
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.iluwatar.servicestub;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
6+
7+
public class AppTest {
8+
@Test
9+
void shouldExecuteWithoutException() {
10+
assertDoesNotThrow(() -> App.main(new String[]{}));
11+
}
12+
}
Lines changed: 27 additions & 0 deletions
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+
import java.util.Random;
6+
7+
class RealSentimentAnalysisServerTest {
8+
9+
@Test
10+
void testPositiveSentiment() {
11+
RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 0);
12+
assertEquals("Positive", server.analyzeSentiment("Test"));
13+
}
14+
15+
@Test
16+
void testNegativeSentiment() {
17+
RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 1);
18+
assertEquals("Negative", server.analyzeSentiment("Test"));
19+
}
20+
21+
@Test
22+
void testNeutralSentiment() {
23+
RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 2);
24+
assertEquals("Neutral", server.analyzeSentiment("Test"));
25+
}
26+
27+
}
Lines changed: 27 additions & 0 deletions
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)