Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Encouraging Good Behavior With JUnit 5 Test Interfaces

DZone's Guide to

Encouraging Good Behavior With JUnit 5 Test Interfaces

Getting developers to write automated tests and follow recommended patterns will always be a struggle for every organization, but JUnit 5 makes it a little easier.

· Performance Zone ·
Free Resource

Maintain Application Performance with real-time monitoring and instrumentation for any application. Learn More!

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 that 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 those that do.

@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 a 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 inheritances, 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 for 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 for test interfaces. I hope this article piqued your interest in 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

  1. Without Automated Testing You Are Building Legacy
  2. Four Common Mistakes That Make Automated Testing More Difficult
  3. This Post -> Encouraging Good Behavior with JUnit 5 Test Interfaces
  4. Conditionally Disabling and Filtering Tests in JUnit 5

Collect, analyze, and visualize performance data from mobile to mainframe with AutoPilot APM. Learn More!

Topics:
automated testing ,junit ,junit 5 ,spring ,performance

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}