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..5521af080ce3 --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/App.java @@ -0,0 +1,45 @@ +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..cd0175972fda --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/RealSentimentAnalysisServer.java @@ -0,0 +1,45 @@ +package com.iluwatar.servicestub; + +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 + * 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; + } + + @SuppressWarnings("java:S2245") // Safe use: Randomness is for simulation/testing only + 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..87f5d33e2ed8 --- /dev/null +++ b/service-stub/src/main/java/com/iluwatar/servicestub/StubSentimentAnalysisServer.java @@ -0,0 +1,27 @@ +package com.iluwatar.servicestub; + +/** + * Stub implementation of SentimentAnalysisServer. + * Returns deterministic sentiment based on input keywords. + */ + +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..5f91eb8bf75d --- /dev/null +++ b/service-stub/src/test/java/com/iluwatar/servicestub/AppTest.java @@ -0,0 +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[] {})); + } +} 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..6a7c8557093e --- /dev/null +++ b/service-stub/src/test/java/com/iluwatar/servicestub/RealSentimentAnalysisServerTest.java @@ -0,0 +1,26 @@ +package com.iluwatar.servicestub; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +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); + } +}