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:
- What is a unit test and why is it valuable?
- How do I write a proper or useful unit test?
- If unit tests don’t feel right, what other type of test will give me confidence?
- Testing in production sounds useful. How does that fit into my development workflow?
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:
- Translation, Identification
- Abstraction, Aggregation (organizational), Integration
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.
Most of the code I have written ends up moving data around. A frequent pattern is:
- Call an HTTP Endpoint
- Do some translation or filtering
- Possibly call another HTTP Endpoint
- Return some status or data back to the client
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:
- In some sense, we could ignore the network code and focus on the filtering logic because we have an interface to work with.
- We could focus on the network code and make sure that we will be able to return a
com.consul.Service
. Imagine you wrote your filtering logic and realized that your data model was wrong. Yikes! (Maybe this isn’t so bad with a good IDE?)
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:
- That test looks gross.
- Why is “returns some services” helpful? What is it helping define?
- When will this test fail? Will its failure bring a sense of urgency to the developer?
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:
- We haven’t tested for exceptional cases (when the network call fails, when a timeout happens).
- If we wanted to test these cases, we would need control the behavior of the
RestTemplate
. This would require injecting theRestTemplate
into our@Service
probably with constructor injection as this is a hard requirement of our class.
@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.
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:
- Why is this test useful?
- Are we doing this just to confirm a particular code path works? (coverage)
- In the interest on maintainability, is this test necessary for future developers?
- Is using our own
someWrappedException
really useful in this use case?
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:
- verified that this method can retrieve data (from the integration test)
- created an explicit contract for failures that is independent of Spring’s RestTemplate (from the exception test function)
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:
- number of services
- fields in the service objects
- variations in other filter parameters
My questions around this function are:
- Should this method be static or not? Should a class be instantiated to call this method?
- Who owns this method? Where in a package should something like this go?
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:
- What value is gained from testing this function?
- If I mock out the return of
getServices()
, I am essentially unit testingfilterServices
. Do these “aggregation” methods usually mean higher levels of testing are required to “cover” the behavior of the system?
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 mock
s and when
s.
“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!