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}