001package io.avaje.inject;
002
003import java.util.function.Consumer;
004
005/**
006 * Build a bean context with options for shutdown hook and supplying test doubles.
007 * <p>
008 * We would choose to use BeanContextBuilder in test code (for component testing) as it gives us
009 * the ability to inject test doubles, mocks, spy's etc.
010 * </p>
011 *
012 * <pre>{@code
013 *
014 *   @Test
015 *   public void someComponentTest() {
016 *
017 *     MyRedisApi mockRedis = mock(MyRedisApi.class);
018 *     MyDbApi mockDatabase = mock(MyDbApi.class);
019 *
020 *     try (BeanContext context = BeanContext.newBuilder()
021 *       .withBeans(mockRedis, mockDatabase)
022 *       .build()) {
023 *
024 *       // built with test doubles injected ...
025 *       CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
026 *       coffeeMaker.makeIt();
027 *
028 *       assertThat(...
029 *     }
030 *   }
031 *
032 * }</pre>
033 */
034public interface BeanContextBuilder {
035
036  /**
037   * Create the bean context without registering a shutdown hook.
038   * <p>
039   * The expectation is that the BeanContextBuilder is closed via code or via using
040   * try with resources.
041   * </p>
042   * <pre>{@code
043   *
044   *   // automatically closed via try with resources
045   *
046   *   try (BeanContext context = BeanContext.newBuilder()
047   *     .withNoShutdownHook()
048   *     .build()) {
049   *
050   *     String makeIt = context.getBean(CoffeeMaker.class).makeIt();
051   *   }
052   *
053   * }</pre>
054   *
055   * @return This BeanContextBuilder
056   */
057  BeanContextBuilder withNoShutdownHook();
058
059  /**
060   * Specify the modules to include in dependency injection.
061   * <p/>
062   * This is effectively a "whitelist" of modules names to include in the injection excluding
063   * any other modules that might otherwise exist in the classpath.
064   * <p/>
065   * We typically want to use this in component testing where we wish to exclude any other
066   * modules that exist on the classpath.
067   *
068   * <pre>{@code
069   *
070   *   @Test
071   *   public void someComponentTest() {
072   *
073   *     EmailServiceApi mockEmailService = mock(EmailServiceApi.class);
074   *
075   *     try (BeanContext context = BeanContext.newBuilder()
076   *       .withBeans(mockEmailService)
077   *       .withModules("coffee")
078   *       .withIgnoreMissingModuleDependencies()
079   *       .build()) {
080   *
081   *       // built with test doubles injected ...
082   *       CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
083   *       coffeeMaker.makeIt();
084   *
085   *       assertThat(...
086   *     }
087   *   }
088   *
089   * }</pre>
090   *
091   * @param modules The names of modules that we want to include in dependency injection.
092   * @return This BeanContextBuilder
093   */
094  BeanContextBuilder withModules(String... modules);
095
096  /**
097   * Set this when building a BeanContext (typically for testing) and supplied beans replace module dependencies.
098   * This means we don't need the usual module dependencies as supplied beans are used instead.
099   */
100  BeanContextBuilder withIgnoreMissingModuleDependencies();
101
102  /**
103   * Supply a bean to the context that will be used instead of any
104   * similar bean in the context.
105   * <p>
106   * This is typically expected to be used in tests and the bean
107   * supplied is typically a test double or mock.
108   * </p>
109   *
110   * <pre>{@code
111   *
112   *   Pump pump = mock(Pump.class);
113   *   Grinder grinder = mock(Grinder.class);
114   *
115   *   try (BeanContext context = BeanContext.newBuilder()
116   *     .withBeans(pump, grinder)
117   *     .build()) {
118   *
119   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
120   *     coffeeMaker.makeIt();
121   *
122   *     Pump pump1 = context.getBean(Pump.class);
123   *     Grinder grinder1 = context.getBean(Grinder.class);
124   *
125   *     assertThat(pump1).isSameAs(pump);
126   *     assertThat(grinder1).isSameAs(grinder);
127   *
128   *     verify(pump).pumpWater();
129   *     verify(grinder).grindBeans();
130   *   }
131   *
132   * }</pre>
133   *
134   * @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements
135   * @return This BeanContextBuilder
136   */
137  BeanContextBuilder withBeans(Object... beans);
138
139  /**
140   * Add a supplied bean instance with the given injection type.
141   * <p>
142   * This is typically a test double often created by Mockito or similar.
143   * </p>
144   *
145   * <pre>{@code
146   *
147   *   Pump mockPump = ...
148   *
149   *   try (BeanContext context = BeanContext.newBuilder()
150   *     .withBean(Pump.class, mockPump)
151   *     .build()) {
152   *
153   *     Pump pump = context.getBean(Pump.class);
154   *     assertThat(pump).isSameAs(mock);
155   *
156   *     // act
157   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
158   *     coffeeMaker.makeIt();
159   *
160   *     verify(pump).pumpSteam();
161   *     verify(pump).pumpWater();
162   *   }
163   *
164   * }</pre>
165   *
166   * @param type The dependency injection type this bean is target for
167   * @param bean The supplied bean instance to use (typically a test mock)
168   */
169  <D> BeanContextBuilder withBean(Class<D> type, D bean);
170
171  /**
172   * Use a mockito mock when injecting this bean type.
173   *
174   * <pre>{@code
175   *
176   *   try (BeanContext context = BeanContext.newBuilder()
177   *     .withMock(Pump.class)
178   *     .withMock(Grinder.class, grinder -> {
179   *       // setup the mock
180   *       when(grinder.grindBeans()).thenReturn("stub response");
181   *     })
182   *     .build()) {
183   *
184   *
185   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
186   *     coffeeMaker.makeIt();
187   *
188   *     // this is a mockito mock
189   *     Grinder grinder = context.getBean(Grinder.class);
190   *     verify(grinder).grindBeans();
191   *   }
192   *
193   * }</pre>
194   */
195  BeanContextBuilder withMock(Class<?> type);
196
197  /**
198   * Use a mockito mock when injecting this bean type additionally
199   * running setup on the mock instance.
200   *
201   * <pre>{@code
202   *
203   *   try (BeanContext context = BeanContext.newBuilder()
204   *     .withMock(Pump.class)
205   *     .withMock(Grinder.class, grinder -> {
206   *
207   *       // setup the mock
208   *       when(grinder.grindBeans()).thenReturn("stub response");
209   *     })
210   *     .build()) {
211   *
212   *
213   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
214   *     coffeeMaker.makeIt();
215   *
216   *     // this is a mockito mock
217   *     Grinder grinder = context.getBean(Grinder.class);
218   *     verify(grinder).grindBeans();
219   *   }
220   *
221   * }</pre>
222   */
223  <D> BeanContextBuilder withMock(Class<D> type, Consumer<D> consumer);
224
225  /**
226   * Use a mockito spy when injecting this bean type.
227   *
228   * <pre>{@code
229   *
230   *   try (BeanContext context = BeanContext.newBuilder()
231   *     .withSpy(Pump.class)
232   *     .build()) {
233   *
234   *     // setup spy here ...
235   *     Pump pump = context.getBean(Pump.class);
236   *     doNothing().when(pump).pumpSteam();
237   *
238   *     // act
239   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
240   *     coffeeMaker.makeIt();
241   *
242   *     verify(pump).pumpWater();
243   *     verify(pump).pumpSteam();
244   *   }
245   *
246   * }</pre>
247   */
248  BeanContextBuilder withSpy(Class<?> type);
249
250  /**
251   * Use a mockito spy when injecting this bean type additionally
252   * running setup on the spy instance.
253   *
254   * <pre>{@code
255   *
256   *   try (BeanContext context = BeanContext.newBuilder()
257   *     .withSpy(Pump.class, pump -> {
258   *       // setup the spy
259   *       doNothing().when(pump).pumpWater();
260   *     })
261   *     .build()) {
262   *
263   *     // or setup here ...
264   *     Pump pump = context.getBean(Pump.class);
265   *     doNothing().when(pump).pumpSteam();
266   *
267   *     // act
268   *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
269   *     coffeeMaker.makeIt();
270   *
271   *     verify(pump).pumpWater();
272   *     verify(pump).pumpSteam();
273   *   }
274   *
275   * }</pre>
276   */
277  <D> BeanContextBuilder withSpy(Class<D> type, Consumer<D> consumer);
278
279  /**
280   * Build and return the bean context.
281   *
282   * @return The BeanContext
283   */
284  BeanContext build();
285}