This article is part of my blog series on automated testing promoting my new Pluralsight course Effective Automated Testing with Spring.
For the third entry in my blog series, I initially planned to cover the differences between unit and integration testing. However, Nicolas Frankel covered that area pretty well recently. So instead I will cover another valuable topic: JUnit 5.
JUnit 5, released in September of 2017, is the first major release for the popular JUnit testing framework in a little over a decade. I recently presented on JUnit 5 at Lava One Conf in Hawaii in January. If you have heard about JUnit 5, but are not yet familiar with it, you can check out my presentation here, as well as the JUnit 5 User Guides.
While researching for my presentation, one new feature in JUnit 5 really caught my eye was the ability to declare tests on default methods in interfaces. This feature caught my eye because two issues I frequently face are encouraging developers to write automated tests and promoting consistent patterns across the enterprise. In this article we are going to look at how test interfaces can help accomplish both of these goals.
Properly RESTful APIs
REST is a very popular way of communicating over HTTP. Or, really, I should say that a lot of organizations say they are using REST when really their “RESTful” APIs exist little above the Swamp of POX.
Frequent offenses I have seen include returning a 200 status code despite errors having occurred, verbs in the URI (e.g.: resource/addNewResource
), and incorrect HTTP method usage (e.g.: POST
when GET
should had be used or the inverse). I could continue, but REST, despite being a well defined specification, is very rarely followed.
So the scenario we are going to run through is demonstrating how test interfaces can be used to both test a RESTful API and encourage that API to follow the actual REST specification. A minor note for REST hardliners, I will actually be going only to Level 2 on the Richardson Maturity Scale. No hypermedia controls in this demo.
GETting Started
When writing a new REST API, I often start by implementing the GET
endpoints. No particular reason why, it’s just my personal approach. However, before I begin writing production code, I need to first write my tests, or rather test interfaces.
The first interface I am writing is a parent interface that my other test interfaces will extend off of. More on them later.
public interface EndpointTest { String baseEndpoint(); MockMvc getMockMvc(); }
This interface only has two methods: the base endpoint my controller will reside at, and a reference to the MockMvc I will use to test the Controller. This demo is being implemented in my Spring Boot demonstrator project.
Extending off the above interface, I will create another interface that will exercise the GET
endpoints of; returning all resources, returning a single resource, and then a negative test of trying to retrieve a non-existent resource.
public interface GetResourceEndpointTest<T, I> extends EndpointTest { I getExistingResource(); I getNonExistingResoruce(); T foundResource(); String getFoundResourceJsonContent(); OngoingStubbing<T> mockExistingBehavior(); OngoingStubbing<List<T>> mockFindAllResourcesBehavior(); OngoingStubbing<T> mockNonExistingBehavior(); @Test default void testExistingResource() throws Exception { mockExistingBehavior().thenReturn(foundResource()); getMockMvc().perform(get(baseEndpoint() + "/" + getExistingResource())).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(content().json(getFoundResourceJsonContent())); } @Test default void testGetAllResources() throws Exception { mockFindAllResourcesBehavior().thenReturn(Arrays.asList(foundResource())); getMockMvc().perform(get(baseEndpoint())).andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(content().json("[" + getFoundResourceJsonContent() + "]")); } @Test default void testNonExistingResource() throws Exception { mockNonExistingBehavior().thenReturn(null); getMockMvc().perform(get(baseEndpoint() + "/" + getNonExistingResoruce())).andExpect(status().isNotFound()); } }
In the above interface, I’m trying to enforce several standards.
1.) The root endpoint returns a list of all the resources.
2.) Adding an identifier to the path of the URI will return only a single instance of the resource.
3.) Giving the identifier of a non-existent resource will return a 404. This is all pretty simple REST stuff, but I’ve seen far more APIs not following this than are.
@SpringJUnitWebConfig(HotelApplication.class) @WebMvcTest(controllers = CustomerController.class, secure = false) public class TestCustomersController implements GetResourceEndpointTest<Customer, Long> { @Autowired private MockMvc mockMvc; @MockBean private CustomerService service; @Override public String baseEndpoint() { return "/customers"; } @Override public MockMvc getMockMvc() { return mockMvc; } @Override public Long getExistingResource() { return 1L; } @Override public Long getNonExistingResoruce() { return 0L; } @Override public Customer foundResource() { return new Customer(1L, "Joe", "Blow", "Kokomo", "Jr.", new Date(0L)); } @Override public String getFoundResourceJsonContent() { return "{\"firstName\": \"Joe\", \"lastName\" : \"Blow\", \"middleName\" : \"Kokomo\", \"suffix\" : \"Jr.\",\"dateOfLastStay\" : \"1970-01-01T00:00:00.000+0000\" }"; } @Override public OngoingStubbing<Customer> mockExistingBehavior() { return when(service.findCustomerById(getExistingResource())); } @Override public OngoingStubbing<Customer> mockNonExistingBehavior() { return when(service.findCustomerById(getNonExistingResoruce())); } @Override public OngoingStubbing<List<Customer>> mockFindAllResourcesBehavior() { return when(service.findAllCustomers()); } }
While there are lot of methods, the actual implementation work involved in each method is only a single line. So, there isn’t too much overhead involved in implementing the interface.
After doing the work on implementing my controller class, executing the test class returns three passing tests, the tests I added as default methods in GetResourceEndpointTest.
Mixing and Matching with Interfaces
The typical REST API is going to do more than the simple retrieval of all resources or a single resource by ID. Many REST APIs will allow for the adding of new resources, updating resources, the ability to query resources, and maybe even deleting resources. However, not every REST API is going to necessarily implement all those behaviors.
This is where being able to use interfaces is nice. While Java disallows multiple inheritance, a class can implement as many interfaces as your heart desires. If your REST API is pure read, you can simply implement the GET and Search interfaces:
public class TestCustomersController implements GetResourceEndpointTest<Customer, Long>, SearchEndpointTest
If you don’t want to allow querying of resources but do need some create and delete functionality, you can just implement the GET, POST/PUT, and DELETE interfaces:
public class TestCustomersController implements GetResourceEndpointTest<Customer, Long>, PutPostEndpointTesting, DeleteResourceEndpointTest<Long>
The flexibility of being able to implement multiple interfaces is a key advantage over the simple inheritance that previous versions of JUnit offered. Rather it is implementing a REST API, a DAO, or some other pattern your organization uses, implementations typically implement only a subset the full feature set a pattern offers.
Finding the reasonable divisions in how patterns will be implemented and creating test interfaces for each of those divisions means developers using those test interfaces won’t be forced into implementing functionality just to make tests pass.
Conclusion
Getting developers to write automated tests and follow recommended patterns will always be a struggle at every organization. However new features in JUnit 5 (like the test interfaces we just demoed in this article) will hopefully make those tasks easier.
My demo, admittedly, is a bit on the raw side. A few issues I ran into while working through the demo was difficulty remembering which interface the methods in my concrete test class came from and a little bit of method overload. I counted 22 methods in a class implementing all my test interfaces. You can view all the code for my demo here.
A few iterations and some collaboration with other developers would probably iron those issues out. I can say working on my demo definitely increased, not dampened, my enthusiasm of test interfaces. I hope this article piqued your interest into using test interfaces at your organization as well.
Researching and using JUnit 5 has made me realize just how much was missing in JUnit 4. JUnit 4 was great in 2006, but a lot has changed since then. Not the least of which: the expectations and needs out of our testing frameworks. I plan on returning to JUnit 5 again in the future, but hopefully this article demonstrated some of the new possibilities JUnit 5 brings to the table.
Automated Testing Series
- Without Automated Testing You Are Building Legacy
- Four Common Mistakes That Make Automated Testing More Difficult
- This Post –> Encouraging Good Behavior with JUnit 5 Test Interfaces
- Conditionally Disabling and Filtering Tests in JUnit 5
- What’s New in JUnit 5.1
- Fluent Assertions with AssertJ
- Why Am I Writing This Test?
- What’s New in JUnit 5.2