Home Java And again about tests. A real life approach to code testing

And again about tests. A real life approach to code testing

by admin

I think almost everyone has encountered the such an opinion : writing tests is difficult, all examples of writing tests are given for the simplest cases, and in real life they do not work. I, on the other hand, have gotten the impression in recent years that writing tests is very easy, even trivial *. The author of the above comment goes on to say that it would be nice to make an example of a complex application and show how to test it. I will try to do just that.
*) Writing the tests themselves is really elementary. Creating an infrastructure that makes it easy to write the tests is a bit more difficult.
I am not a theorist, I am a practitioner ("I am not Pushkin by nature, I am Belinsky" ©). So I will use some ideas from Test Driven Development, Data Driven Development, Behaviour Driven Development, but I can’t always justify my choices. Mainly my reasoning will be: "it’s easier" or "it’s more convenient". I am well aware of the fact that my prescriptions do not suit everyone, so please, firstly, think about my arguments anyway, and secondly, do not consider this text as a textbook.
So, drum roll :

Complicated application

This application exists in reality, but it’s not public, so I can’t show the actual code. The scope is PaaS, platform-as-a-service. Specifically, to automate the launch of client applications on some kind of virtual infrastructure. It will run inside large enterprises. The architecture is pretty standard: a relational database, Hibernate on top, then a web interface, all managed by Spring framework. We have two tools on the side: one of them talks to the API of the virtual infrastructure, and the second one connects to every virtual machine we created via SSH, and starts something there, so the virtual machine gets all the necessary software and the necessary pieces of the client application, and runs those pieces. Honestly, this is the most complex application I’ve written in my life, I think it’ll do as an example. The language is Java, with Scala thrown in.

Spring and tests. Testing business logic with mock tests

Spring applications are very easy to test. One of the reasons to use Spring is that it’s easy to test with. "The rabbit is the one who lives in the burrow. The burrow is where the rabbit lives" ©
Interface :

public interface DeploymentService {Deployment deploy(Application application, DeploymentDescription deploymentDescription);}

Implementation :

@Servicepublic class DeploymentServiceImpl implements DeploymentService {private DeploymentRepository deploymentRepository;private DeploymentMaker deploymentMaker;private VirtualInfrastructureService virtualInfrastructureService;@Autowiredpublic DeploymentServiceImpl(DeploymentRepository deploymentRepository, DeploymentMaker deploymentMaker, VirtualInfrastructureService virtualInfrastructureService) {this.deploymentRepository = deploymentRepository;this.deploymentMaker = deploymentMaker;this.virtualInfrastructureService = virtualInfrastructureService;}public Deployment deploy(Application application, deployentdescription deployentdescription) {//creating a new Deployment objectDeployment deployment = deploymentMaker.makeDeployment(application, deploymentDescription);deploymentRepository.save(deployment);try {virtualInfrastructureService.launchVirtualMachines(deployment.getVirtualMachineDescriptors());} catch (VirtualInfrastructureException e) {throw new DeploymentUnsuccsessfullException(e);}return deployment;}}

Basically, it’s clear what it does. Of course the actual code is a bit more complicated, but not by much. First we test the DeploymentService.deploy() method with the JMock library

public class DeploymentServiceMockTest extends MockObjectTestCase {private DeploymentRepository deploymentRepository = mock(DeploymentRepository.class);private DeploymentMaker deploymentMaker = mock(DeploymentMaker.class);private VirtualInfrastructureService virtualInfrastructureService = mock(VirtualInfrastructureService.class);private DeploymentServiceImpl deploymentService;public void setUp() {deploymentService = new DeploymentServiceImpl(deploymentRepository, deploymentMaker, virtualInfrastructureService);}public void testSuccessfulDeploymentSavedAndVMsLaunched() {final Application app = makeValidApplication();final DeploymentDescription dd = makeValidDeploymentDescription();final Deployment deployment = helperMethodToCreateDeployment();checking(new Expectations(){{one(deploymentMaker).makeDeployment(app, dd);will(returnValue(deployment));one(deploymentRepository).save(deployment);one(virtualInfrastructureService).launchVirtualMachines(deployment.getVirtualMachineDescriptors());}});deploymentService.deploy(application, deploymentDescription);}public void testExceptionIsTranslated() {final Application app = makeValidApplication();final DeploymentDescription dd = makeValidDeploymentDescription();final Deployment deployment = helperMethodToCreateDeployment();checking(new Expectations(){{one(deploymentMaker).makeDeployment(app, dd);will(returnValue(deployment));one(deploymentRepository).save(deployment);one(virtualInfrastructureService).launchVirtualMachines(deployment.getVirtualMachineDescriptors());will(throwException(new VirtualInfrastructureException("error message")));}});` try {deploymentService.deploy(application, deploymentDescription);fail("Expected DeploymentUnsuccsessfullException!");} catch (DeploymentUnsuccsessfullException e) {//expected}}

What’s important and interesting here? First, mock tests allow us to test isolated methods without regard to what those methods call. Second, writing tests very much affects the structure of the code. The first version of the deploy() method, of course, didn’t call DeploymentMaker.makeDeployment(), but the same method inside DeploymentServiceImpl. When I started writing the test, I found that at this point I wasn’t interested in writing tests for all of the options that makeDeployment does. They have nothing to do with the actions in the deploy() method, which just needs to write a new object to the database, and start the process of creating virtual machines. So I put the makeDeployment() logic into a separate helper class. I will test it in a completely different way, because the state of the application and deploymentDescription objects matter for its operation. In addition, I found that once DeploymentMaker is tested, I can use it in other tests to create test data. By the way, JMock has the ability to do mocks not only for interfaces, but also for object instances. To do that, add setImpostorizer(ClassImpostorizer.INSTANCE) to setUp(). I’m sure other moc libraries have something similar.
To finish with this service, all that is left is :

Testing interaction with the database

As I wrote above, we use Hibernate to write our objects to and read them from the database. One of the rules of writing good tests is "Don’t test libraries". In this case, it means that we can trust the authors of Hibernate that they have already tested every possible aspect of writing and reading various object graphs. What we need to confirm with tests is the correctness of our mappings. Plus it’s a good idea to write a small amount of tests, i.e. run DeploymentService.deploy() on a real base and make sure there are no problems.
As far as I know, the recommended way to test the interaction with the databases is as follows: each test method works in a transaction, and at the end of the test a rollback is done. Frankly speaking, I don’t like it. The way we use allows us to test more complex database operations with multiple transactions. For this we use Hypersonic, a SQL-compatible database written in Java and able to work in memory. All of our database tests create a Spring context that uses Hypersonic instead of real PostgreSQL or MySQL. Specific details are beyond the scope of this post, if you want details, write and I’ll tell you.
An abstract class is created as the basis for all our tests. In fact, we used ORMUnit which stupidly recreates the whole database structure before every test. If you use a real database, you can get old before all the tests go through. But with Hypersonic, everything happens really fast. Really, really!

public class DeploymentRepositoryTest extends HibernatePersistenceTest {@Autorwiredprivate DeploymentRepository deploymentRepository;public void testSaveAndLoad() {Deployment deployment = DeploymentMother.makeSimpleDeployment();deploymentRepository.add(deployment);Deployment loadedDeployment = deploymentRepository.getById(deployment.getId());assertDeploymentEquals(deployment, loadedDeployment);}private void assertDeploymentEquals(Deployment expected, Deployment actual) {//you can do anything here. The easiest thing is to write Deployment.equals(...), which will compare all fields// or use EqualsBuilder (it seems to be in Apache Commons-lang). Or just compare the id.}}

Note the DeploymentMother. We have a custom designation for helper classes that create entities. We use these entities for tests. Our DeploymentMother has the following methods : makeSimpleDeployment(), makeMutliVmDeploymentWithMasterSlaveReplication(), makeFailedDeployment(), makeStartedDeploymentWithFailedVM(), and so on. Basically, this is an implementation of one of the poor man’s DDD variants. Personally, I prefer this approach to reading data from YAML or XML for the same reason I prefer Scala over Groovy – type checking at compile time. If I change something in my classes (and with enough tests refactoring goes from dangerous to the most enjoyable) the compiler shows me immediately what problems will occur in tests and what I should pay attention to.
When working with Hibernate, the most interesting part starts when writing complex queries. We use a very useful library Arid pojos (by the same author as ORMUnit), which allows you to avoid writing a huge pile of homogeneous query call code. For example, to select all the deployments that are ready to run, all you need to do is a) write a query named findDeploymentsReadyToLaunch in the Hibernate mapping, and define the List<Deployment> findDeploymentsReadyToLaunch() method in the DeploymentRepository interface. And that’s it, on startup arid-pojos will generate code that will run that exact request. Again, we’re not testing libraries, so all we need to do is create test data and make sure that what we expect is returned from the database. We add in DeploymentRepositoryTest:

public void testRetrieveReadyToLaunch() {for (int i=0; i<5; i++) {deploymentRepository.add(DeploymentMother.makeReadyToLaunchDeployment());}deploymentRepository.add(DeploymentMother.makeNewDeployment());List<Deployment> result = deploymentRepository.findDeploymentsReadyToLaunch(); assertEquals(5, result.size()); for (Deployment d : result) { assertTrue(deployment.isReadyToLaunch()); } //it is clear that it would be nice to compare whether the records selected, but that you yourself, okay? }

A small digression : the problems in the previous examples

In principle, the above test examples work fine. What’s the problem? The problem is that they are not very easy to read. What we are trying to achieve is to make it easy to reconstruct the requirements for the project and the code from the tests. What can we do to make even such simple tests easier to read? Use intention-revealing method names, i.e. method names that reveal intentions (this is a bit from BDD). For example, name the test not testSaveAndLoad, but testSavedDeploymentLoadedCorrectly. Not testRetrieveReadyToLaunch, but testOnlyReadyToLaunchDeploymentsRetrieved.
Next, assertEquals(5, result.size()) – requires a bit of effort to understand what the programmer wanted to say. Better instead, in your TestUtils (you have TestUtils, right?!) create an assertSize(int expected, Collection collection) method. Or better yet :
In TestUtils:

public static void T assertCollection(int expectedSize, ElementAsserter<T> asserter, Collection<T> actual) {assertSize(expectedSize, actual.size());for (T element : actual) {asserter.assertElement(element);}}public static abstract class ElementAsserter<T> {public void assertElement(T element) {if (!checkElement(element)) fail(getFailureDescription(element));}protected abstract boolean checkElement(T element);//redefine this method so that it tells you exactly what is wrong with theprotected String getFailureDescription(T element) object {return "The element is no good"}}

And then in our test we can do so :

ElementAsserter readyToLaunch = new ElementAsserter<Deployment> () {protected boolean checkElement(Deployment d) {return d.isReadyToLaunch();}protected String getFailureDescription(Deployment d) {return String.format("Deployment with id %d and name %s is NOT ready to be launched!");}}private void assertAllReadyToLaunch(int expectedSize, List<Deployment> deployments) {TestUtils.assertCollection(expectedSize, a, deployments);}public void testOnlyReadyToLaunchDeploymentsRetrieved() {for (int i=0; i<5; i++) {deploymentRepository.add(DeploymentMother.makeReadyToLaunchDeployment());}deploymentRepository.add(DeploymentMother.makeNewDeployment());assertAllReadyToLaunch(5, deploymentRepository.findDeploymentsReadyToLaunch());}

You can also hide creation of the five required objects into a separate method, to make it even clearer. There are no limits to perfection. Why all this? Because the programmer has no excuse to write new tests. If all he has to do is two lines of code (call an already written method to create some objects and define the check of some condition), then tests become very easy to write. And the process gives you the incomparable pleasure of knowing that your code can be run directly live – everything will work.

What’s next?

That’s all for today. If there is any interest from hubralubs, there will be a sequel in a couple of days, in which I plan to tell about (our) approach to testing the whole application, communicating with external services, and answer questions. There will also be more about "complex infrastructure for simple tests".

You may also like