Lab 9: Domain Repository
Description¶
In this lab you'll expand the domain objects used for coffee orders, retrieve an order from a collection-like Repository, and incorporate these into the API controller.
Goals¶
- Learn how to extract information from a URI path
- Learn how to add non-component objects to the ApplicationContext
- Reinforce injecting dependencies to components
a. New CoffeeOrder Aggregate¶
In this step, we'll introduce the files needed to expand our domain objects to include the "owning" object (or aggregate): the CoffeeOrder
.
The coffee order can contain multiple coffee items.
-
Copy the CoffeeOrder.java class to the
domain
package in thesrc
directory. -
Copy the CoffeeOrderResponse.java class to the
adapter.in.api
package in thesrc
directory. -
Copy the CoffeeOrderWebTest.java to overwrite the existing one in the
test
directory. -
In the
CoffeeOrderController
, replace the existing method with the following:@GetMapping("/api/coffee/orders/{id}") public CoffeeOrderResponse coffeeOrder(@PathVariable("id") long orderId) { CoffeeItem coffeeItem = new CoffeeItem("small", "latte", "milk"); coffeeItem.setId(99L); CoffeeOrder coffeeOrder = new CoffeeOrder("Ted", LocalDateTime.of(2020, 10, 11, 12, 13)); coffeeOrder.add(coffeeItem); coffeeOrder.setId(orderId); return CoffeeOrderResponse.from(coffeeOrder); }
URI Path Template Variables
For more information on how template variables work, such as
@PathVariable
and{id}
used above, see: https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-ann-requestmapping-uri-templates -
Run the
CoffeeOrderWebTest
, which should pass. -
Run the application and access it via the browser: http://localhost:8080/api/coffee/orders/42
Browsers Prefer XML
Because browsers "prefer" XML in terms of what they accept (as they don't have JSON in their Accept header), you will see XML returned instead of JSON. That's totally fine! If you'd like, you can use
curl
or a tool likeInsomnia
orPostman
to force the response to be JSON by setting theAccept:
header to beapplication/json
, e.g.:curl -v -H "Accept: application/json" "http://localhost:8080/api/coffee/orders/23"
b. Copy Repository Files¶
Now we need a place to store (and retrieve) those Coffee Orders. For this, we'll use the Domain Repository Pattern.
-
Copy the CoffeeOrderRepository.java interface file into your code directory in the
domain
package. -
Copy the InMemoryCoffeeOrderRepository.java implementation also into the
domain
package.
c. Configure In-Memory Repository¶
Since the InMemoryCoffeeOrderRepository
is not a Spring-managed bean, but a Domain Object, we need to provide a bridge so that Spring can become aware of it, without coupling the Domain to the Spring framework.
To do this, we'll use Spring's @Configuration
and @Bean
annotations.
-
Create a new
src
classCoffeeOrderRepositoryConfig
in thecom.welltestedlearning.coffeekiosk
package. -
Add the
@Configuration
annotation to the class, so that Spring will find it during component scanning. -
Add the following method:
@Bean public CoffeeOrderRepository inMemoryCoffeeOrderRepository() { return new InMemoryCoffeeOrderRepository(); }
This will tell Spring to add the
InMemoryCoffeeOrderRepository
instance to itsApplicationContext
so that it can be auto-wired wherever needed. -
To test that you've configured things correctly so far, open up the
CoffeeKioskApplicationTests
test and add:@Autowired CoffeeOrderRepository coffeeOrderRepository;
-
Run the
contextLoads
test (in theCoffeeKioskApplicationTests
class), which will cause Spring to attempt to auto-wire the repository.If things are configured correctly, the test will pass.
If not, then you'll get an error message like:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.welltestedlearning.coffeekiosk.domain.CoffeeOrderRepository' available:
-
Add the following test (to
CoffeeKioskApplicationTests
):@Test public void sampleDataWasLoaded() throws Exception { assertThat(coffeeOrderRepository.findAll()) .hasSize(1); }
and run it. It will fail because the repository is empty. We'll fix that in the next step.
d. Pre-load Repository¶
So that we'll always have some sample data loaded into the repository, we'll create a startup loader similar to what we did in Lab 3.
-
Create a new
src
class,SampleDataLoader
in the root package (com.welltestedlearning.coffeekiosk
). -
Annotate it as a component, and implement the appropriate interface so that it gets run upon startup.
-
Add a
private final
instance field for theCoffeeOrderRepository
and have it auto-wired via constructor injection. -
In the
run()
method, instantiate aCoffeeItem
andCoffeeOrder
like you did in Step A above, then call the repository'ssave()
method to save it. -
Run the
CoffeeKioskApplicationTests
test and if everything was done correctly, the tests should pass. -
Run all of the tests, which should also now be passing.
e. GET By Order ID¶
Now that the repository is available and has data loaded, we can return that sample Coffee Order in the GET-mapped method in the controller.
-
Open
CoffeeOrderController
and add aprivate final
instance field for theCoffeeOrderRepository
and have it auto-wired via constructor injection. -
In the GET-mapped
coffeeOrder
method, replace the hard-coded creation of the coffee item & order with a lookup from the repository, e.g.:CoffeeOrder coffeeOrder = coffeeOrderRepository.findById(orderId).get();
The
.get()
is required asfindById
returns anOptional
. For now, we'll ignore the potential for a null. -
Run the application and using your browser or
curl
, etc., go to: http://localhost:8080/api/coffee/orders/23
What happens if you use a number other than 23 here?
f. Test Configuration¶
If you try to run the "sliced" CoffeeOrderWebTest
, you'll see that it fails to start.
This is because the controller now has an auto-wired dependency on the repository that isn't initialized for tests annotated with only @WebMvcTest
.
There are several options. Try each one and run the tests to see how it works.
-
Load the configuration and sample data classes during the test. This is straightforward, but can get more complex as you have more dependencies on various configuration files. To do this, add the following annotation to the test above the class name:
@Import({CoffeeOrderRepositoryConfig.class, SampleDataLoader.class})
-
Change to the more comprehensive -- but slower --
@SpringBootTest
annotation, instead of@WebMvcTest
. Remove the class annotations and add these:@SpringBootTest @AutoconfigureMockMvc
-
Create a Mockito mock for the repository, which is a programmable stub, using the
@MockBean
annotation. This is often used, but can lead to other issues of over-mocking.For this example, return to just having the
@WebMvcTest(CoffeeOrderController.class)
as the annotation. Then add the following code to the class:@MockBean CoffeeOrderRepository coffeeOrderRepository; @BeforeEach public void initRepo() { CoffeeItem coffeeItem = new CoffeeItem("small", "latte", "milk"); coffeeItem.setId(99L); CoffeeOrder coffeeOrder = new CoffeeOrder("Ted", LocalDateTime.of(2020, 10, 11, 12, 13)); coffeeOrder.add(coffeeItem); coffeeOrder.setId(23L); org.mockito.Mockito.when(coffeeOrderRepository.findById(23L)).thenReturn(Optional.of(coffeeOrder)); }
Aggregate Design Reference
See the series on Aggregate Design by Vernon Vaughn: https://kalele.io/effective-aggregate-design/