My thoughts on Testing (in Spring)

really though, I need help

Over the last year, I have been tasked with mentoring a few engineers (both interns and full-timers). My weakest skill is writing and structuring code and I have put in a bit more energy as of late to try and understand testing. My main concerns are:

The following article doesn’t necessarily lead to any conclusions but will hopefully provide a concrete example for discussion. Perhaps ~you~ can help me!

What is a Unit Test?

In my opinion, a unit test exists to test lines of code that don’t depend on integration but do perform a “unit of work.” With this definition, I can try and define if a function is or isn’t performing a unit of work. After looking at several of our code bases, I frequently see two buckets of functions:

Under the assumption that there are two buckets of functions, let’s take a detour and analyze the general flow of our programs.

The “testing pyramid” is usually brought up to visualize quantity of tests and, in my opinion, to illustrate that higher level tests build “on top” of unit tests. However, I believe that a code base is best visualized like the roots of a tree where each root and split represents a code path (I suppose you could also use the branches of a tree as a similar analogy…). Unlike the testing pyramid, I have noticed that the “ends” of the roots actually end up being integration points. To me, this suggests that your units of work are actually built on top of integration tests. An application trace is perhaps the next best visualization as it is both functional and intuitive.

Example Trace via https://buoyant.io/wp-content/uploads/2017/07/buoyant-zipkin-trace-overview.png

Most of the code I have written ends up moving data around. A frequent pattern is:

If you were tasked with the above task, where would you start writing this? Would you start with task 2 because you can easily write a unit test for it? Would you start at step 1 and move to step 3 to understand the data models before writing step 2? I am sure there are guidelines on when and how to write the above code in a book somewhere (queue a more senior engineer’s experience). From my experience, I usually find that defining the data model is the first step. After, defining interfaces can be useful if you realize there are pieces that are likely to change implementation. I haven’t written much code, but I haven’t come across many times where using an interface has really helped when changing implementation over time. After you have models and interfaces, you are usually ready to begin implementation. Like I mentioned earlier, most of the code paths I have seen really start at the integration layer.

My thought process: filtering data from Consul

Reading Data from Consul

In the case above, I would start by writing a Service that calls out to our data source.

@Service
public class ConsulService {
public ConsulService () {}
public List<com.consul.Service> getServices() {
// some network code
return null;
}
}

Lets say my goal was to filter the services in Consul based on a complex set of tags or timestamps (the point being some operation that the REST API doesn’t support). At this point, I see two options:

Perhaps there is some information out that that would support one path versus of the other but I would prefer to write the network code to ensure I have a foundational data model to build on.

In the spirit of TDD, we can write a test case to force us to define the behavior of our function.

@RunWith(SpringRunner.class)
@ContextConfiguration(ConsulService.class)
public class ConsulServiceTest {
@Autowired
ConsulService consulService;

@Test
public void getServicesReturnsSomeServices(){
Assert.assertTrue(consulService.getServices().size() > 0);
}
}

Here are some things I think of when I see the function above:

One of the first things we can say is that this is an integration test because its success depends on the real state of the world (the connectivity to Consul, the number of services in Consul). This might be why the test seems gross. The whole point of writing this test is to make sure whatever network code we write will indeed call and return Consul Services. We could potentially create a Service in Consul in a @Before clause to guarantee that we would get back at least one service. As stated above, the test could fail for reasons outside of the test suite’s control.

While the test is less than ideal, lets leave it as it is and focus on the network code.

public List<com.consul.Service> getServices() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(
"http://consul.mycompany.internal:8500/v1/agent/services",
com.consul.Service.class
);
}

Lets assume our test passes at this point. Here are some thoughts I have:

@Service
public class ConsulService {
RestTemplate restTemplate; @Autowired
public ConsulService (RestTemplate restTemplate) {
this.RestTemplate = restTemplate
}
public List<com.consul.Service> getServices() {
return restTemplate.getForObject(
"http://consul.mycompany.internal:8500/v1/agent/services",
com.consul.Service.class
);
}
}

Fortunately, the Consul API does follow some conventions so there may be a way to use the @Repository annotation to avoid writing any of the above code.

Indicates that an annotated class is a “Repository”, originally defined by Domain-Driven Design (Evans, 2003) as “a mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects”.

Teams implementing traditional Java EE patterns such as “Data Access Object” may also apply this stereotype to DAO classes, though care should be taken to understand the distinction between Data Access Object and DDD-style repositories before doing so. This annotation is a general-purpose stereotype and individual teams may narrow their semantics and use as appropriate.

~ link

I will ignore this for the time being to continue to analyze what is left.

With our new function, we can now inject our own version of a RestTemplate to control behavior (like purposely throw a RestClientException). If we want to wrap the exception in our own class’s custom exception, we could use the following to test that behavior:

@RunWith(SpringRunner.class)
@ContextConfiguration(ConsulServiceTest.class, ConsulService.class)
public class ConsulServiceTest {
@Bean
RestTemplate mockRestTempalte() {
return Mockito.mock(RestTemplate.class);
}
@Autowired
RestTemplate mockRestTemplate;
@Autowired
ConsulService consulService;

...
@Test(expected = someWrappedException.class)
public void getServicesShouldThrowSomeWrappedException() throws...
{
Mockito.when(mockRestTempalte.getForObject(...)).thenThrow(...)
consulService.getServices()
}
}

The same questions come to mind:

To be honest, I don’t really have answers to these questions and am still forming my opinions as to which code paths are useful to test. However, I do consider this a unit test because control flow doesn’t really leave our function to interact with an integration point.

At this point, we have:

Filtering Data From Consul

Our filter function might look like the following:

public List<Service> filterServices(List<Service> services, String someRequirement, int anotherRequirement)

I am not interested in detailing what the function does but let’s assume we don’t need to make any network calls in this function. To me, this is the clear “unit of work” that is important for our business. We might want to test particular edges cases:

My questions around this function are:

I don’t think an object really needs to be involved to perform this particular operation nor would a class in the future needed for any change in behavior. To be honest, I would likely end up placing this function in some static XXXUtility class. Moreover, the test for this file wouldn’t need to use the SpringRunner as it shouldn’t depend on any Spring features. Although obvious, this is a good indication that we are writing a unit test and not an integration test.

Unifying the Two

Somewhere in our codebase, we will need to perform both operations:

function List<Services> GetFilteredServices() {
List<Services> services = consulService.getServices();
return Util.filterServices(services);
}

We can see that getServices is a function that performs integration and filterServices performs a useful unit of work. Circling back to the “two bucket” assumption, I would classify this function, GetFilteredServices, as a function that is written to perform aggregation of the two methods. Again, I have similar questions as before:

We could make the method slightly more complicated:

@Scheduled(cron="*/5 * * * * MON-FRI")
function List<Services> UpdateSpecialServices() {
List<Services> services = consulService.getServices();
services = Util.filter(services);
services = Util.updateSomeField(service);
consulService.updateServices(services);
return services;
}

Each of these functions could be tested individually but I don’t really see any sense in Unit testing UpdateSpecialServices using a bunch of mocks and whens.

“Testing” in Production

Using the above example, we might not feel confident that we have written the “cleanest” code or have the highest coverage report. However, we may have written enough code and tests where developers that approach the codebase can easily identify the most important functions necessary for change (probably filter or updateSomething). Even before deploying, we are confident that our service can reliably integrate with Consul. However, our efforts won’t mean much until we can run this in production and provide some business value. For the best description of types of tests we could run in production, check out some of the pictures in the following article:

Monitoring would likely give us the ability to do some manual inspect if our application is working. Chaos Engineering might be useful to understand how our service works over time (perhaps our service has a memory leak after many failed network calls?). Canarying sounds useful but might require us to modify our application if we don’t want to operate on all Consul services (back to updating the filter function!).

Conclusion

As with most things in Software Development, you will need to use discretion when choosing your testing strategies. The whole point of doing any sort of testing is building confidence that our software works. Doing any testing “for the sake of testing” usually won’t result in much value and will generally feel pretty boring.

I am definitely not the first person to talk about testing so I am sure there are many great resources out there that might detail more about software development (particularly how to write code from start to deploy). If you have any great resources that covers this topic, please leave a comment below! Thank you for reading!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store