View Javadoc
1   package com.guinetik.hexafun.testing;
2   
3   import com.guinetik.hexafun.HexaApp;
4   import com.guinetik.hexafun.fun.Result;
5   import java.util.function.Consumer;
6   import java.util.function.Function;
7   import java.util.function.Predicate;
8   
9   /**
10   * A fluent API for testing HexaFun use cases.
11   * @param <I> The input type of the use case
12   * @param <O> The output type of the use case
13   */
14  public class UseCaseTest<I, O> {
15      private final HexaApp app;
16      private final String useCaseName;
17      private I input;
18      private O output;
19      private Exception exception;
20      private boolean executed = false;
21  
22      UseCaseTest(HexaApp app, String useCaseName) {
23          this.app = app;
24          this.useCaseName = useCaseName;
25      }
26  
27      /**
28       * Specify the input for the use case.
29       * @param input The input to provide to the use case
30       * @return This test instance
31       */
32      public UseCaseTest<I, O> with(I input) {
33          this.input = input;
34          return this;
35      }
36  
37      /**
38       * Execute the use case and verify the result is successful.
39       * @param verifier A consumer that receives the result for verification
40       * @return This test instance
41       */
42      public UseCaseTest<I, O> expectOk(Consumer<O> verifier) {
43          executeIfNeeded();
44          if (exception != null) {
45              throw new AssertionError("Expected successful result but got exception: " + exception.getMessage(), exception);
46          }
47          
48          if (output instanceof Result) {
49              Result<?> result = (Result<?>) output;
50              if (result.isFailure()) {
51                  throw new AssertionError("Expected successful result but got failure: " + result.error());
52              }
53              @SuppressWarnings("unchecked")
54              O unwrapped = (O) result.get();
55              verifier.accept(unwrapped);
56          } else {
57              verifier.accept(output);
58          }
59          
60          return this;
61      }
62  
63      /**
64       * Execute the use case and verify the result is a failure.
65       * @param errorVerifier A consumer that receives the error message for verification
66       * @return This test instance
67       */
68      public UseCaseTest<I, O> expectFailure(Consumer<String> errorVerifier) {
69          executeIfNeeded();
70          if (exception != null) {
71              errorVerifier.accept(exception.getMessage());
72              return this;
73          }
74          
75          if (output instanceof Result) {
76              Result<?> result = (Result<?>) output;
77              if (result.isSuccess()) {
78                  throw new AssertionError("Expected failure but got successful result: " + result.get());
79              }
80              errorVerifier.accept(result.error());
81          } else {
82              throw new AssertionError("Expected Result type but got: " + output.getClass().getName());
83          }
84          
85          return this;
86      }
87  
88      /**
89       * Execute the use case and verify the result matches a predicate.
90       * @param predicate A predicate to test the result
91       * @param description Description of what the predicate checks
92       * @return This test instance
93       */
94      public UseCaseTest<I, O> expect(Predicate<O> predicate, String description) {
95          executeIfNeeded();
96          if (exception != null) {
97              throw new AssertionError("Expected result but got exception: " + exception.getMessage(), exception);
98          }
99          
100         if (!predicate.test(output)) {
101             throw new AssertionError("Expected " + description + " but was not satisfied by " + output);
102         }
103         
104         return this;
105     }
106 
107     /**
108      * Execute the use case and verify it throws a specific exception.
109      * @param exceptionClass Expected exception class
110      * @return This test instance
111      */
112     public UseCaseTest<I, O> expectException(Class<? extends Exception> exceptionClass) {
113         executeIfNeeded();
114         if (exception == null) {
115             throw new AssertionError("Expected exception of type " + exceptionClass.getName() + " but no exception was thrown");
116         }
117         
118         if (!exceptionClass.isInstance(exception)) {
119             throw new AssertionError("Expected exception of type " + exceptionClass.getName() 
120                 + " but got " + exception.getClass().getName(), exception);
121         }
122         
123         return this;
124     }
125 
126     /**
127      * Map the result using a transformer function for further verification.
128      * @param mapper Function to transform the result
129      * @param <T> Target type
130      * @return A new test instance with the transformed result
131      */
132     public <T> UseCaseTest<I, T> map(Function<O, T> mapper) {
133         executeIfNeeded();
134         if (exception != null) {
135             throw new AssertionError("Cannot map result: an exception occurred: " + exception.getMessage(), exception);
136         }
137         
138         UseCaseTest<I, T> mappedTest = new UseCaseTest<>(app, useCaseName);
139         mappedTest.input = this.input;
140         mappedTest.executed = true;
141         
142         try {
143             mappedTest.output = mapper.apply(output);
144         } catch (Exception e) {
145             mappedTest.exception = e;
146         }
147         
148         return mappedTest;
149     }
150 
151     /**
152      * Execute the use case if it hasn't been executed yet.
153      */
154     private void executeIfNeeded() {
155         if (executed) {
156             return;
157         }
158 
159         try {
160             output = app.invokeByName(useCaseName, input);
161         } catch (Exception e) {
162             exception = e;
163         }
164 
165         executed = true;
166     }
167 }