Microservice Test Strategy— Unit Testing

ScottDiesel
6 min readJul 5, 2022

In this four part series, I aim to de-mystify what each form of Microservice testing really is. For too many years I’ve scoured articles using greatly oversimplified examples and confusing test terminology. Here, I’ll provide actual Microservice projects I’ve created in Github. They’re designed to look very similar to what you’ll see across companies, but still simplified for easy understanding and extensibility.

https://github.com/ScottKnox/vehicle-service is what we’ll be using throughout this article.

With these, we’ll show how to build confidence in your product via Automation Testing. Confidence that each part of the application is functioning correctly, that those parts integrate together to form a cohesive whole, and that each application itself integrates with one another to form your Microservice architecture.

Why Unit Test?

Let us begin at the very bottom of the test pyramid.

Unit testing is the cheapest, most reliable, and simple to maintain form of automated testing we have available to us. This is exactly why our test pyramid shows it as the largest grouping of coverage we should maintain. Many companies greatly underestimate the power of Unit testing. More than once I’ve heard burnt-out developers tell me they’re a waste of time. More than once I’ve opened projects to find clearly rushed tests that lack coverage of critical business logic. Even worse, I’ve found tests that only appear to cover that code.

These tests intend to verify functionality in the code itself, testing each class and its behaviors in isolation. Thanks to powerful mocking libraries, as you’ll soon see, this is a lot easier to do than you might think. If developers cumulatively build up accurate coverage for much of their classes and the logic therein, they can be confident that each part of the application is functioning as it should.

Coverage VS Accuracy

Here is what I mean by accurate unit test coverage. “Coverage” refers to the amount of overall application code and logic being tested by your unit tests. Code quality analyzers like Sonarqube measure this, but can easily be fooled by poor quality tests to make managers happy with green letters and percentages.

Most companies aim for 80% of their application code to have unit test coverage. Of far greater importance though, is that the coverage be accurate. That is, are the unit tests correctly mocking out dependencies, or are they inadvertently using the real ones and getting invalid results? Are they asserting on the right fields, or something that will always pass even if the code were changed improperly?

What Can’t a Unit Test Cover?

While Unit tests are super powerful testing assets, they do have limitations. When our application is properly Unit tested, we can be confident each part is doing its job, and assume they’ll integrate nicely together. We’ll need higher levels of testing to be confident that’s the case. The core concept to understand is that we’re mainly testing integration as we move up the test pyramid. We should be testing as much logic as possible here, in simple, speedy Unit test land.

How to Unit Test

Alright; we know what Unit testing is and why it’s done. Let’s see what that all actually means in the real world.

The project you cloned from Github earlier has everything you need to follow along and practice. Since we’re Unit testing, you’ll need only to open the project in IntelliJ. Feel free to PM me any issues or questions you have. 😉

Start by opening the VehicleService class from the directory shown.

This is the service level of the application. You’ll generally find most business logic at this level, and thus a plethora of testing opportunity. In this example we simply see a getVehicles method that pulls vehicles from a database repository. It filters through them to only return those that have an image.

To understand how great Unit tests really are, imagine all the ways we could test the “getVehicles” method is correctly returning vehicles. We could, for example, run the actual Vehicle Service and write a test that calls the actual /vehicles endpoint and verifies only vehicles with images were returned. That unfortunately requires spinning up the service, a database, seeding the database through various means, and all sorts of other complexities.

We can get nearly that same exact coverage from a Unit test.

Go ahead and open up “VehicleServiceTest”

Feel free to right click on the test, and run it within IntelliJ. In just a few milliseconds, the test generated stubbed vehicle data with the conditions we need, returned them from a mocked Repository class that acted like our database, and verified the results. Let’s work through the test code.

First, we can’t test the “VehicleService” class in isolation if it’s calling a real “VehicleRepository.” We’d have to spin up an actual local instance of the database for that to be possible, which would then cause this test to be an “Integration” test, introducing more complexities. Instead, we mock it here using Mockito.

Once we’ve told our VehicleService class under test to use that mocked out Repository (using the ReflectionTestUtils), we can control what data is returned from it.

Look how easily we can control what data is returned from our database. We could simulate any number of scenarios, and assert that our business logic is performing as expected. When developers make future changes to that logic, we can always run these tests again to ensure they haven’t broken the way something worked before.

We then call the “getVehicles” method on our VehicleService. The mocked out repository within it returns our defined data, and we can assert that data came back as expected.

Exercise

Let’s add some new business logic, and write a unit test for it!

Go back to the “VehicleService” class and add the extra condition shown. Say the Boss has had some bad experiences with Lexus vehicles, and doesn’t want those to be returned either. (This company’s management is questionable) Currently, our unit tests only cover if vehicles that lack images are filtered. This new logic presents us with additional conditions to cover.

@Test
public void getVehicles_IncludingLexusBrand_ReturnsNoLexus() {

}

Add another test for the new condition. Test naming schemes are always up for debate, but I’ve always enjoyed thingUnderTest_Conditions_ExpectedResult() as a general format.

@Test
public void getVehicles_IncludingLexusBrand_ReturnsNoLexus() {
List<Vehicle> dbVehicles = new ArrayList<>();
Vehicle vehicle1 = new Vehicle();
vehicle1.setId("dbb0c2c3-997c-465b-bb06-b2efb7dad2f7");
vehicle1.setBrand("Dodge");
vehicle1.setModel("Charger");
vehicle1.setImageUrl("https://images.com/dodge-charger.jpg");

Vehicle vehicle2 = new Vehicle();
vehicle2.setId("acc0c2c3-997c-465b-bb06-b2efb7dad2f7");
vehicle2.setBrand("Lexus");
vehicle2.setModel("IS-250");
vehicle2.setImageUrl("https://images.com/lexus-250.jpg");

dbVehicles.add(vehicle1);
dbVehicles.add(vehicle2);

when(repository.findAll()).thenReturn(dbVehicles);

List<Vehicle> vehicles = vehicleService.getVehicles();

// Only the Dodge should be returned
Assert.assertEquals(1, vehicles.size());
Assert.assertEquals("Dodge", vehicles.get(0).getBrand());
}

Setup the scenario data, configure the behavior of our mock repository, call the method under test, and verify the behavior matches what our estranged boss has asked for.

Sweet! Now we’ve got some coverage. If developers down the line were to modify that logic, we’d get failures here to let us know behavior had deviated from what was previous expected of the application. Sometimes that’s okay, and we’ll merely need to update these tests to match the new application logic.

Conclusion

If we were to create the same tests for most critical classes within the Vehicle Service application, we’d have confidence that we could make changes to each part without breaking existing functionality. Done correctly, this can provide you major application test coverage with tests that run incredibly fast and reliably. Yet, we still don’t have confidence each of these tested parts will integrate together properly to form the working application. They should, but if our boss is making us filter a vehicle brand they don’t like, should probably won’t cut it.

Let’s move on to Integration testing!

--

--

ScottDiesel

I’m a Software Engineer aiming to share the experience I’ve gained working with Expedia, the Federal Government, and now, Jack Henry in the banking sector.