From 7c11cba19a5ca55765f98388f85ddb3921cf6b91 Mon Sep 17 00:00:00 2001 From: John Klint Date: Sat, 22 Mar 2025 21:56:20 +0100 Subject: [PATCH 1/3] Add Service Stub Pattern using Sentiment Analysis example --- pom.xml | 1 + service-stub/README.md | 153 ++++++++++++++++++ service-stub/pom.xml | 27 ++++ .../java/com/iluwatar/servicestub/App.java | 44 +++++ .../RealSentimentAnalysisServer.java | 39 +++++ .../servicestub/SentimentAnalysisServer.java | 15 ++ .../StubSentimentAnalysisServer.java | 24 +++ .../com/iluwatar/servicestub/AppTest.java | 12 ++ .../RealSentimentAnalysisServerTest.java | 27 ++++ .../StubSentimentAnalysisServerTest.java | 27 ++++ 10 files changed, 369 insertions(+) create mode 100644 service-stub/README.md create mode 100644 service-stub/pom.xml create mode 100644 service-stub/src/main/java/com/iluwatar/servicestub/App.java create mode 100644 service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java create mode 100644 service-stub/src/main/java/com/iluwatar/servicestub/SentimentAnalysisServer.java create mode 100644 service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java create mode 100644 service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java create mode 100644 service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java create mode 100644 service-stub/src/test/java/com/iluwatar/servicestub/StubSentimentAnalysisServerTest.java diff --git a/pom.xml b/pom.xml index 33ebf63e0e61..327ed11e982a 100644 --- a/pom.xml +++ b/pom.xml @@ -224,6 +224,7 @@ table-inheritance bloc map-reduce + service-stub diff --git a/service-stub/README.md b/service-stub/README.md new file mode 100644 index 000000000000..352f6b321392 --- /dev/null +++ b/service-stub/README.md @@ -0,0 +1,153 @@ +--- +title: "Service Stub Pattern in Java: Simplifying Testing with Stub Implementations" +shortTitle: Service Stub +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." +category: Structural +language: en +tag: + - Testing + - Decoupling + - Dummy Services + - Dependency Injection +--- + +## Also known as + +* Dummy Service +* Fake Service + +## Intent of Service Stub Pattern + +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. + +## Detailed Explanation of Service Stub Pattern with Real-World Example + +Real-world example + +> 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. + +In plain words + +> Use a fake service to return predictable results without relying on external systems. + +Wikipedia says + +> A test stub is a dummy component used during testing to isolate behavior. + +## Programmatic Example of Service Stub Pattern in Java + +We define a `SentimentAnalysisService` interface and provide two implementations: + +1. **RealSentimentAnalysisServer**: Simulates a slow, random sentiment analysis system. +2. **StubSentimentAnalysisServer**: Returns a deterministic result based on input keywords. + +### Example Implementation +Both the real service and the stub implement the interface below. +```java +public interface SentimentAnalysisServer { + String analyzeSentiment(String text); +} +``` +The real sentiment analysis class returns a random response for a given input and simulates the runtime by sleeping +the Thread for 5 seconds. The Supplier allows injecting controlled sentiment values during testing, ensuring +deterministic outputs. +```java +public class RealSentimentAnalysisServer implements SentimentAnalysisServer { + + private final Supplier sentimentSupplier; + + public RealSentimentAnalysisServer(Supplier sentimentSupplier) { + this.sentimentSupplier = sentimentSupplier; + } + + public RealSentimentAnalysisServer() { + this(() -> new Random().nextInt(3)); + } + + @Override + public String analyzeSentiment(String text) { + int sentiment = sentimentSupplier.get(); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral"; + } +} +``` +The stub implementation simulates the real sentiment analysis class and provides a deterministic output +for a given input. Additionally, its runtime is almost zero. +```java +public class StubSentimentAnalysisServer implements SentimentAnalysisServer { + + @Override + public String analyzeSentiment(String text) { + if (text.toLowerCase().contains("good")) { + return "Positive"; + } + else if (text.toLowerCase().contains("bad")) { + return "Negative"; + } + else { + return "Neutral"; + } + } +} + +``` +Here is the main function of the App class (entry point to the program) +```java +@Slf4j + public static void main(String[] args) { + LOGGER.info("Setting up the real sentiment analysis server."); + RealSentimentAnalysisServer realSentimentAnalysisServer = new RealSentimentAnalysisServer(); + String text = "This movie is soso"; + LOGGER.info("Analyzing input: {}", text); + String sentiment = realSentimentAnalysisServer.analyzeSentiment(text); + LOGGER.info("The sentiment is: {}", sentiment); + + LOGGER.info("Setting up the stub sentiment analysis server."); + StubSentimentAnalysisServer stubSentimentAnalysisServer = new StubSentimentAnalysisServer(); + text = "This movie is so bad"; + LOGGER.info("Analyzing input: {}", text); + sentiment = stubSentimentAnalysisServer.analyzeSentiment(text); + LOGGER.info("The sentiment is: {}", sentiment); + } +``` +## When to Use the Service Stub Pattern in Java + +Use the Service Stub pattern when: + +* Testing components that depend on external services. +* The real service is slow, unreliable, or unavailable. +* You need predictable, predefined responses. +* Developing offline without real service access. + +## Real-World Applications of Service Stub Pattern in Java + +* Simulating APIs (payments, recommendation systems) during testing. +* Bypassing external AI/ML models in tests. +* Simplifying integration testing. + +## Benefits and Trade-offs of Service Stub Pattern + +Benefits: + +* Reduces dependencies. +* Provides predictable behavior. +* Speeds up testing. + +Trade-offs: + +* Requires maintaining stub logic. +* May not fully represent real service behavior. + +## Related Java Design Patterns + +* [Proxy](https://java-design-patterns.com/patterns/proxy/) +* [Strategy](https://java-design-patterns.com/patterns/strategy/) + +## References and Credits + +* [Martin Fowler: Test Stubs](https://martinfowler.com/articles/mocksArentStubs.html) diff --git a/service-stub/pom.xml b/service-stub/pom.xml new file mode 100644 index 000000000000..d6aad385ba45 --- /dev/null +++ b/service-stub/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + service-stub + + + 17 + 17 + UTF-8 + + + + org.junit.jupiter + junit-jupiter + test + + + + \ No newline at end of file diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/App.java b/service-stub/src/main/java/com/iluwatar/servicestub/App.java new file mode 100644 index 000000000000..4409b8426abf --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/App.java @@ -0,0 +1,44 @@ +package com.iluwatar.servicestub; +import lombok.extern.slf4j.Slf4j; + +/** + * A Service Stub is a dummy implementation of an external service used during development or + * testing. The purpose is to provide a lightweight "stub" when the real service may not always be + * available (or too slow to use during testing). + * + *

This implementation simulates a simple sentiment analysis program, where a text is analyzed to + * deduce whether it is a positive, negative or neutral sentiment. The stub returns a response based + * on whether the analyzed text contains the words "good" or "bad", not accounting for stopwords or + * the underlying semantic of the text. + * + *

The "real" sentiment analysis class simulates the processing time for the request by pausing + * the execution of the thread for 5 seconds. In the stub sentiment analysis class the response is + * immediate. In addition, the stub returns a deterministic output with regard to the input. This + * is extra useful for testing purposes. + */ + + +@Slf4j +public class App { + /** + * Program entry point. + * + * @param args command line args + */ + public static void main(String[] args) { + LOGGER.info("Setting up the real sentiment analysis server."); + RealSentimentAnalysisServer realSentimentAnalysisServer = new RealSentimentAnalysisServer(); + String text = "This movie is soso"; + LOGGER.info("Analyzing input: {}", text); + String sentiment = realSentimentAnalysisServer.analyzeSentiment(text); + LOGGER.info("The sentiment is: {}", sentiment); + + LOGGER.info("Setting up the stub sentiment analysis server."); + StubSentimentAnalysisServer stubSentimentAnalysisServer = new StubSentimentAnalysisServer(); + text = "This movie is so bad"; + LOGGER.info("Analyzing input: {}", text); + sentiment = stubSentimentAnalysisServer.analyzeSentiment(text); + LOGGER.info("The sentiment is: {}", sentiment); + + } +} diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java new file mode 100644 index 000000000000..394931ab17f5 --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java @@ -0,0 +1,39 @@ +package com.iluwatar.servicestub; + +import java.util.Random; +import java.util.function.Supplier; + +public class RealSentimentAnalysisServer implements SentimentAnalysisServer { + /** + * A real sentiment analysis implementation would analyze the input string using, e.g., NLP and + * determine whether the sentiment is positive, negative or neutral. Here we simply choose a random + * number to simulate this. The "model" may take some time to process the input and we simulate + * this by delaying the execution 5 seconds. + * + * @param text the input string to analyze + * @return sentiment classification result (Positive, Negative, or Neutral) + */ + + private final Supplier sentimentSupplier; + + // Constructor + public RealSentimentAnalysisServer(Supplier sentimentSupplier) { + this.sentimentSupplier = sentimentSupplier; + } + + public RealSentimentAnalysisServer() { + this(() -> new Random().nextInt(3)); + } + + @Override + public String analyzeSentiment(String text) { + int sentiment = sentimentSupplier.get(); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral"; + } + } + diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/SentimentAnalysisServer.java b/service-stub/src/main/java/com/iluwatar/servicestub/SentimentAnalysisServer.java new file mode 100644 index 000000000000..174f81f4298c --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/SentimentAnalysisServer.java @@ -0,0 +1,15 @@ +package com.iluwatar.servicestub; + +/** + * Sentiment analysis server interface to be implemented by sentiment analysis services. + */ + +public interface SentimentAnalysisServer { + /** + * Analyzes the sentiment of the input text and returns the result. + * + * @param text the input text to analyze + * @return sentiment classification result + */ + String analyzeSentiment(String text); +} diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java b/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java new file mode 100644 index 000000000000..3cc16dbdf260 --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java @@ -0,0 +1,24 @@ +package com.iluwatar.servicestub; + +public class StubSentimentAnalysisServer implements SentimentAnalysisServer { + + /** + * Fake sentiment analyzer, always returns "Positive" if input string contains the word "good", + * "Negative" if the string contains "bad" and "Neutral" otherwise. + * + * @param text the input string to analyze + * @return sentiment classification result (Positive, Negative, or Neutral) + */ + @Override + public String analyzeSentiment(String text) { + if (text.toLowerCase().contains("good")) { + return "Positive"; + } + else if (text.toLowerCase().contains("bad")) { + return "Negative"; + } + else { + return "Neutral"; + } + } +} diff --git a/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java b/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java new file mode 100644 index 000000000000..b77d86de1836 --- /dev/null +++ b/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java @@ -0,0 +1,12 @@ +package com.iluwatar.servicestub; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class AppTest { + @Test + void shouldExecuteWithoutException() { + assertDoesNotThrow(() -> App.main(new String[]{})); + } +} diff --git a/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java b/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java new file mode 100644 index 000000000000..e1d0c393d6b8 --- /dev/null +++ b/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java @@ -0,0 +1,27 @@ +package com.iluwatar.servicestub; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import java.util.Random; + +class RealSentimentAnalysisServerTest { + + @Test + void testPositiveSentiment() { + RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 0); + assertEquals("Positive", server.analyzeSentiment("Test")); + } + + @Test + void testNegativeSentiment() { + RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 1); + assertEquals("Negative", server.analyzeSentiment("Test")); + } + + @Test + void testNeutralSentiment() { + RealSentimentAnalysisServer server = new RealSentimentAnalysisServer(() -> 2); + assertEquals("Neutral", server.analyzeSentiment("Test")); + } + +} diff --git a/service-stub/src/test/java/com/iluwatar/servicestub/StubSentimentAnalysisServerTest.java b/service-stub/src/test/java/com/iluwatar/servicestub/StubSentimentAnalysisServerTest.java new file mode 100644 index 000000000000..3c8ae96829a6 --- /dev/null +++ b/service-stub/src/test/java/com/iluwatar/servicestub/StubSentimentAnalysisServerTest.java @@ -0,0 +1,27 @@ +package com.iluwatar.servicestub; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class StubSentimentAnalysisServerTest { + + private final StubSentimentAnalysisServer stub = new StubSentimentAnalysisServer(); + + @Test + void testPositiveSentiment() { + String result = stub.analyzeSentiment("This is a good product"); + assertEquals("Positive", result); + } + + @Test + void testNegativeSentiment() { + String result = stub.analyzeSentiment("This is a bad product"); + assertEquals("Negative", result); + } + + @Test + void testNeutralSentiment() { + String result = stub.analyzeSentiment("This product is average"); + assertEquals("Neutral", result); + } +} From d727e991211c2cd29556bc7c0686da367224d88d Mon Sep 17 00:00:00 2001 From: John Klint Date: Sat, 22 Mar 2025 22:35:08 +0100 Subject: [PATCH 2/3] Fix Checkstyle issues --- .../java/com/iluwatar/servicestub/App.java | 1 + .../RealSentimentAnalysisServer.java | 39 +++++++++++-------- .../StubSentimentAnalysisServer.java | 11 ++++-- .../com/iluwatar/servicestub/AppTest.java | 3 +- .../RealSentimentAnalysisServerTest.java | 1 - 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/App.java b/service-stub/src/main/java/com/iluwatar/servicestub/App.java index 4409b8426abf..5521af080ce3 100644 --- a/service-stub/src/main/java/com/iluwatar/servicestub/App.java +++ b/service-stub/src/main/java/com/iluwatar/servicestub/App.java @@ -1,4 +1,5 @@ package com.iluwatar.servicestub; + import lombok.extern.slf4j.Slf4j; /** diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java index 394931ab17f5..fe723dd0ed29 100644 --- a/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java +++ b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java @@ -3,6 +3,11 @@ import java.util.Random; import java.util.function.Supplier; +/** + * Real implementation of SentimentAnalysisServer. + * Simulates random sentiment classification with processing delay. + */ + public class RealSentimentAnalysisServer implements SentimentAnalysisServer { /** * A real sentiment analysis implementation would analyze the input string using, e.g., NLP and @@ -14,26 +19,26 @@ public class RealSentimentAnalysisServer implements SentimentAnalysisServer { * @return sentiment classification result (Positive, Negative, or Neutral) */ - private final Supplier sentimentSupplier; + private final Supplier sentimentSupplier; - // Constructor - public RealSentimentAnalysisServer(Supplier sentimentSupplier) { - this.sentimentSupplier = sentimentSupplier; - } + // Constructor + public RealSentimentAnalysisServer(Supplier sentimentSupplier) { + this.sentimentSupplier = sentimentSupplier; + } - public RealSentimentAnalysisServer() { - this(() -> new Random().nextInt(3)); - } + public RealSentimentAnalysisServer() { + this(() -> new Random().nextInt(3)); + } - @Override - public String analyzeSentiment(String text) { - int sentiment = sentimentSupplier.get(); - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral"; + @Override + public String analyzeSentiment(String text) { + int sentiment = sentimentSupplier.get(); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } + return sentiment == 0 ? "Positive" : sentiment == 1 ? "Negative" : "Neutral"; } +} diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java b/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java index 3cc16dbdf260..87f5d33e2ed8 100644 --- a/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java +++ b/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java @@ -1,5 +1,10 @@ package com.iluwatar.servicestub; +/** + * Stub implementation of SentimentAnalysisServer. + * Returns deterministic sentiment based on input keywords. + */ + public class StubSentimentAnalysisServer implements SentimentAnalysisServer { /** @@ -13,11 +18,9 @@ public class StubSentimentAnalysisServer implements SentimentAnalysisServer { public String analyzeSentiment(String text) { if (text.toLowerCase().contains("good")) { return "Positive"; - } - else if (text.toLowerCase().contains("bad")) { + } else if (text.toLowerCase().contains("bad")) { return "Negative"; - } - else { + } else { return "Neutral"; } } diff --git a/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java b/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java index b77d86de1836..5f91eb8bf75d 100644 --- a/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java +++ b/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java @@ -1,12 +1,11 @@ package com.iluwatar.servicestub; import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; public class AppTest { @Test void shouldExecuteWithoutException() { - assertDoesNotThrow(() -> App.main(new String[]{})); + assertDoesNotThrow(() -> App.main(new String[] {})); } } diff --git a/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java b/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java index e1d0c393d6b8..6a7c8557093e 100644 --- a/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java +++ b/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -import java.util.Random; class RealSentimentAnalysisServerTest { From 9a1cccc856ec2695d29b60ac3114a36f8016dd86 Mon Sep 17 00:00:00 2001 From: John Klint Date: Sat, 22 Mar 2025 23:00:09 +0100 Subject: [PATCH 3/3] Suppress Sonar warning for Random usage in RealSentimentAnalysisServer --- .../com/iluwatar/servicestub/RealSentimentAnalysisServer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java index fe723dd0ed29..cd0175972fda 100644 --- a/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java +++ b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java @@ -26,6 +26,7 @@ public RealSentimentAnalysisServer(Supplier sentimentSupplier) { this.sentimentSupplier = sentimentSupplier; } + @SuppressWarnings("java:S2245") // Safe use: Randomness is for simulation/testing only public RealSentimentAnalysisServer() { this(() -> new Random().nextInt(3)); }