And again about the tests. Real Life Testing Approach

    I think almost everyone came across this opinion : it’s difficult to write tests, all examples of writing tests are given for the simplest cases, but in real life they do not work. But in recent years I have got the impression that writing tests is very simple, even trivial * . The author of the comment mentioned above further says that it would be nice to make an example of a complex application and show how to test it. I'll try to do just that.

    *) Writing the tests themselves is really elementary. Creating an infrastructure that makes it easy to write tests is a bit more complicated.


    I am not a theorist, I am a practitioner (“I am not Pushkin by nature, I am Belinsky” ©). Therefore, I will use some ideas from Test Driven Development, Data Driven Development, Behavior Driven Development, but I can not always justify my choice. Basically, my argument will be: “It's easier,” or “that's more convenient.” Knowing full well that my recipes are not suitable for everyone, I ask, firstly, all the same, to reflect on my argumentation, and, secondly, not to consider this text as a textbook.

    So, the drum roll:

    Sophisticated application



    This application exists in reality, but it is not public, so I can not show the real code. Scope - PaaS, platform-as-a-service. Specifically, the automation of launching client applications on a certain virtual infrastructure. Will work inside large enterprises. The architecture is fairly standard: a relational database, on top of Hibernate, then a web interface, everything is managed by the Spring framework. On the side we have two Pribluda: one is talking to the API of that virtual infrastructure, and the second is connecting to each virtual machine created through SSH, and it starts something there, as a result of which the virtual machine gets all the necessary software and the necessary pieces of the client application, and these pieces starts up. To be honest, this is the most complex application that I have written in my life, I think it will be an example. Language - Java,

    Spring and tests. Testing business logic with mock tests



    Spring applications are very easy to test. One of the reasons for using Spring is that it is easy to test with it. “A rabbit is one who lives in a hole. Nora - this is where the rabbit lives "©

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


    Implementation:
    @Service
    public class DeploymentServiceImpl implements DeploymentService {
      private DeploymentRepository deploymentRepository;
      private DeploymentMaker deploymentMaker;
      private VirtualInfrastructureService virtualInfrastructureService;
      @Autowired
      public DeploymentServiceImpl(DeploymentRepository deploymentRepository, DeploymentMaker deploymentMaker, VirtualInfrastructureService virtualInfrastructureService) {
        this.deploymentRepository = deploymentRepository;
        this.deploymentMaker = deploymentMaker;
        this.virtualInfrastructureService = virtualInfrastructureService;
      }
      public Deployment deploy(Application application, DeploymentDescription deploymentDescription) {
          //создаем новый объект Deployment
          Deployment deployment = deploymentMaker.makeDeployment(application, deploymentDescription);
          deploymentRepository.save(deployment);
          try {
    	virtualInfrastructureService.launchVirtualMachines(deployment.getVirtualMachineDescriptors());
          } catch (VirtualInfrastructureException e) {
    	throw new DeploymentUnsuccsessfullException(e);
          }
          return deployment;
      }
    }
    


    In principle, it is clear what it does. Of course, the real code is a little more complicated, but not by much. First, we test DeploymentService.deploy () method in the forehead using the JMock mock 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 is important and interesting? Firstly, mock tests allow us to test isolated methods without regard to what these methods call. Secondly, writing tests greatly affects the structure of the code. The first variant of the deploy () method, of course, did not call DeploymentMaker.makeDeployment (), but the same method inside DeploymentServiceImpl. When I started writing the test, I found that at this stage I was not interested in writing tests for all the options that makeDeployment performs. They have nothing to do with the actions in the deploy () method, which simply needs to write a new object to the database and start the process of creating virtual machines. So I pulled out the makeDeployment () logic in a separate helper class. I will test it in completely different ways, because the state of application and deploymentDescription objects matters for its operation. In addition, I found that after DeploymentMaker is tested, I can use it in other tests to create test data. By the way, in JMock there is the ability to make moki not only for interfaces, but also for object instances. To do this, add setImpostorizer (ClassImpostorizer.INSTANCE) to setUp (). I am sure that in other mock libraries there is something similar.

    To finish with this service, it remains:

    Database Interaction Testing



    As I wrote above, we use Hibernate to write our objects to the database and read them from it. One of the rules for writing good tests is, “No need to test libraries.” In this case, this means that we can trust the authors of Hibernate in that they have already tested all possible aspects of writing and reading various graphs of objects. What we need to confirm with the help of tests is the correctness of our mappings. Plus it’s nice to write a small number of integration tests, i.e. run DeploymentService.deploy () on a real database and make sure that there are no problems.

    As far as I know, the following method of testing interaction with databases is recommended: each test method works in a transaction, and a rollback is performed at the end of the test. Honestly, I don't like this. The method we use allows us to test more complex database operations that perform several transactions. To do this, we use Hypersonic - an SQL-compatible database written in Java and able to work in memory. All 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; for details, write, I’ll tell you.

    An abstract class is created as the basis for all our tests. We actually used ORMUnit, which stupidly recreates the entire database structure before each test. If you use a real database, you can grow old until all the tests pass. But when using Hypersonic, everything happens very quickly. True true!
    public class DeploymentRepositoryTest extends HibernatePersistenceTest {
      @Autorwired
      private 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) {
        //тут можно как угодно поступить. Самое простое - написать Deployment.equals(...), который сравнит все поля
        //или использовать EqualsBuilder (кажется, он в Apache Commons-lang). Или просто сравнить id. 
      }
    }
    


    Pay attention to DeploymentMother. We have adopted such designations for helper classes that create entities. Such entities are used for tests. Our DeploymentMother has such methods: makeSimpleDeployment (), makeMutliVmDeploymentWithMasterSlaveReplication (), makeFailedDeployment (), makeStartedDeploymentWithFailedVM (), and so on. In principle, this is an implementation of one of the DDD options for the poor. Personally, I prefer this approach to reading data from YAML or XML for the same reason that I prefer Scala rather than Groovy - type checking at compile time. If I change something in my classes (and if there are enough tests, refactoring turns from a dangerous perversion into a pleasant experience), then the compiler will immediately show me what problems will arise in the tests and what you will need to pay attention to.

    When working with Hibernate, the fun part begins when writing complex queries. We use a very useful library Arid pojos (the same author as ORMUnit), which allows you not to write a huge bunch of the same type of request code. For example, to select all deployments that are ready to be launched, it is enough a) to write a query called findDeploymentsReadyToLaunch in the Hibernate mapping, and define the List methodfindDeploymentsReadyToLaunch () in the DeploymentRepository interface. And that’s all, when starting arid-pojos, it will generate code that will run this particular request. Again, we do not test libraries, therefore it is enough for us to create test data and make sure that what we expect is returned from the database. Add to DeploymentRepositoryTest:

    
      public void testRetrieveReadyToLaunch() {
        for (int i=0; i<5; i++) {
          deploymentRepository.add(DeploymentMother.makeReadyToLaunchDeployment());
        }
        deploymentRepository.add(DeploymentMother.makeNewDeployment());
        List result = deploymentRepository.findDeploymentsReadyToLaunch();
        assertEquals(5, result.size());
        for (Deployment d : result) {
          assertTrue(deployment.isReadyToLaunch());
        }
        //понятно, что неплохо бы сравнить, нужные ли записи выбраны, но это вы сами, ладно?
      }
    


    A small digression: problems in previous examples


    Basically, the above test examples work great. What is the problem? The fact that they are not very easy to read. We are trying to ensure that the tests could easily restore the requirements for the project and the code. What can be done to make even such simple tests easier to read? Use intention-revealing method names, i.e. names of methods that reveal intentions (this is already a bit from BDD). For example, to name the test not testSaveAndLoad, but testSavedDeploymentLoadedCorrectly. Not testRetrieveReadyToLaunch, but testOnlyReadyToLaunchDeploymentsRetrieved.

    Further, assertEquals (5, result.size ()) - requires a little effort to understand what the programmer wanted to say. Instead, it is better to create an assertSize (int expected, Collection collection) method in your TestUtils (you have TestUtils, right ?!). Or even better:

    In TestUtils:
    
      public static void T assertCollection(int expectedSize, ElementAsserter asserter, Collection actual) {
        assertSize(expectedSize, actual.size());
        for (T element : actual) {
          asserter.assertElement(element);
        }
      }
      public static abstract class ElementAsserter {
        public void assertElement(T element) {
          if (!checkElement(element)) fail(getFailureDescription(element));
        }
        protected abstract boolean checkElement(T element);
        //переопределите этот метод, чтобы он вам говорил что конкретно не так с объектом
        protected String getFailureDescription(T element) {
          return "Элемент не годится";
        }
      }
    


    And then in our test we can do this:

      ElementAsserter readyToLaunch = new ElementAsserter() {
        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 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 the creation of five necessary objects in a separate method, so that it becomes completely clear. There are no limits to perfection. Why all this? And then, that the programmer has no excuses from writing new tests. If all that is required of him is two lines of code (call the already written method to create some objects and determine the verification of a certain condition), then it becomes very easy to write tests. And the process gives you incomparable pleasure in realizing that your code can be directly run live - everything will work.

    What next?



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

    Also popular now: