Anemic architecture - enemy of testing

Although Rich Model ref seems like a better approach from OOP evangelists' standpoint, Anemic Model ref stays prevailing among enterprise developers. While not being very active in OOP debates, I find Anemic architecture very harmful for testing. To support this claim let me start with a small example of anemic code (full code):

//As seen from the name - this is a representative of the Service Layer
class PersonService {
    //Do some calculations on the Person's data
    PersonStatistics getStatistics(long personId) {
        //using PersonDao to load entity from DB
        Person p = personDao.get(personId);
        PersonStatistics result = new PersonStatistics(p);

        // The actual "calculations"
        if(CollectionUtils.isNotEmpty(p.getRelatives()))
            result.setNumOfRelatives(p.getRelatives().size());
        if(CollectionUtils.isNotEmpty(p.getProjects()))
            result.setNumOfProjects(p.getProjects().size());

        // Some weird logic just for the demo - storing stats in DB if they are not empty
        if((result.getNumOfProjects() != null && result.getNumOfProjects() != 0)
                || (result.getNumOfRelatives() != null && result.getNumOfRelatives() != 0))
            //using StatisticsDao to save results to DB
            statsDao.save(result);
        return result;
    }
}

The logic here is fully concentrated in the service layer. It supposedly should be a Façade that just invokes the actual logic - but in our case it implements all of it. And the rest of the objects are just data structures which is a sign of anemic model. There are couple of ways we can write tests for this.

Mocking - work more, accomplish less

In our first attempt we're going to use mocks. Here are some emerging patterns (full code):

@Test
public class PersonServiceTest {
   public void returnsProjectCount_ifProjectsArePresent() {
       Person p = new Person().setProjects(projects());

       //Mocking PersonDao!
       doReturn(p).when(personDao).get(1L);
       PersonStatistics statistics = service.getStatistics(1L);

       assertEquals(statistics.getNumOfProjects(), (Integer) p.getProjects().size());
   }

   public void savesStatistics_ifProjectsArePresent() {
       Person p = new Person().setProjects(projects());

       //Mocking PersonDao!
       doReturn(p).when(personDao).get(1L);
       service.getStatistics(1L);

       //Verifying that PersonDao was invoked by the service
       //We may also want to check the actual values of the stats,
       //but let's keep it simple here
       verify(statsDao).save(any(PersonStatistics.class));
   }
}

There are 2 types of tests here - those that test the conditions and calculations and those that check the collaboration with other objects. Mocked depended-on objects (DOCs) lead to problems though:

  1. A part of the service logic was to invoke its DOCs with particular params in particular conditions. Because we replaced the real logic with the fake DOC the tests pass when the behaviour of the service satisfies the fake methods. Not the real ones. The real DOC may be working differently - it may actually fail if service passes current params. Thus by using mocks we test our test setup and don’t test the actual communication.
  2. We broke encapsulation. Now the outside world (tests) knows about the internals of our Service - about how it does what it does. If the Service gets refactored (it does the same thing but differently) we’d have to change all the tests that prepared the mocks for it. Imagine if now we wanted to use statsDao.saveOrUpdate() instead of statsDao.save() - several tests would have to be updated while the behaviour didn’t change.

As a result by introducing mocking we worsened the maintainability of our tests as well as decreased the coverage ref ref. But we can fix that - instead of using mocks and writing unit tests we’ll write Component (a.k.a. Integration) Tests.

You’re not an enterprise developer if your code is fast

So now our tests initialize a part of the application, inject the real DOCs and we check it all together (full code):

//Loads a part of the application declared in dao-context.xml
@ContextConfiguration("classpath:/dao-context.xml")
@Transactional @Test
public class PersonServiceComponentTest extends AbstractTestNGSpringContextTests {

    // Fully initialized Service with real dependencies
    @Autowired PersonService service;
    @Autowired StatisticsDao statsDao;
    @Autowired PersonDao personDao;

    public void returnsProjectCount_ifProjectsArePresent() {
        //Saves Person object to DB
        Person p = save(new Person().setProjects(projects());

        //Will actually update DB state
        PersonStatistics statistics = service.getStatistics(p.getId());
        assertEquals(statistics.getNumOfProjects(), (Integer) p.getProjects().size());
    }
    public void savesStatistics_ifProjectsArePresent() {
        //Again - saves Person to DB
        Person p = save(new Person().setProjects(projects());

        service.getStatistics(p.getId());

        //Uses real DAO to actually perform SQL queries
        assertNotNull(statsDao.getPersonStats(p.getId()));
    }
}

Now we test the real stuff so we improved the coverage. If it works in tests - it works in prod (at least the scenarios that we came up with). But the price is high:

  • Now the tests are much (much!) slower. If we run one test in IDE we have to wait for the whole preparation to take place every time. And we (hopefully) run tests many times when we write the code. So it’s going to slow the development down. Also the whole test run is going to be slower. If we take testing seriously and cover thousands of cases it can easily take 30 mins to run the whole suite.
  • These tests are complicated. First of all it’s not always easy to prepare such tests (e.g. you can’t run them using in-memory DB and have to set something up either on localhost or remotely). Also there are a lot more moving parts that you have to be aware of when you write these tests. The data management (creating entities and their associations) can be pretty tricky since Person has nested fields which have to be prepared and be valid.

So while we improved the coverage we paid with the team’s performance.

Solution - Rich Model

Let's recap:

  1. We used mocks - it was bad because maintainability and coverage was poor
  2. We replaced those tests with Component Tests and now they are slow

It's time for a real solution - Rich Model. The rule is simple - if logic can be put into lower layers of the app, it should be done. Here is a modified code (full code PersonService, Person, PersonStatistics):

class PersonService {
   PersonStatistics getStatistics(long personId) {
      Person p = personDao.get(personId);

      //Stats are now built by Person object
      PersonStatistics result = p.buildStatistics();

      //PersonStatistics can tell if it's empty or not
      if(result.isNotEmpty()) statsDao.save(result);
      return result;
   }
}

// Domain Model
class Person {

   // This can be tested without going to DB
   PersonStatistics buildStatistics() {
       PersonStatistics result = new PersonStatistics(this);
       if (CollectionUtils.isNotEmpty(getRelatives()))
              result.setNumOfRelatives(getRelatives().size());
       if (CollectionUtils.isNotEmpty(getProjects()))
              result.setNumOfProjects(getProjects().size());
       return result;
   }
}

class PersonStatistics {

   // This can also be tested without DB
   boolean isNotEmpty() {
       return !((numOfProjects == null || numOfProjects == 0)
                 && (numOfRelatives == null || numOfRelatives == 0));
   }
}

Now we can easily write unit tests for the business logic (full code PersonStatisticsTest, PersonTest):

@Test // No database, no mocks!
public class PersonStatisticsTest {
   public void statsAreNotEmpty_ifProjectsArePresent() {
       PersonStatistics stats = new PersonStatistics(null)
               .setNumOfProjects(positiveInteger());
       assertTrue(stats.isNotEmpty());
   }
}

@Test // No database, no mocks!
public class PersonTest {
   public void returnsProjectCount_ifProjectsArePresent() {
       Person p = new Person().setProjects(projects());

       PersonStatistics statistics = p.buildStatistics();
       assertEquals(statistics.getNumOfProjects(), (Integer) p.getProjects().size());
   }
}
          

And of course we should also check if classes still work together via couple of Component Tests (full code):

@Transactional @Test @ContextConfiguration("classpath:/dao-context.xml")
public class PersonServiceComponentTest extends AbstractTestNGSpringContextTests {
   public void savesStatistics_ifNotEmpty() {
       //saving to real DB
       Person p = save(new Person().setProjects(projects()));

       service.getStatistics(p.getId());
       assertNotNull(statsDao.getPersonStats(p.getId()));
   }
}
          

Note, that we didn’t get rid of Component Tests, but decreased their number. This is a step towards the all important Test Pyramid. So finally we got what we wanted:

  • The coverage is high because we test both the business logic and the interaction between our objects
  • The tests are fast because most (or at least a lot) of the logic is located at the lower layers which are possible to test without complicated setup.
  • The tests are maintainable since we don’t intrude into the private life of our classes (no mocks)
  • Plus (and this is subjective) the code became nicer.

Popup Notes:

  • Rich Model

    An architecture where the logic is kept near the data - in enterprise apps that would be Entities and/or auxiliary classes. Such classes represent the domain we work in and usually are not instantiated/injected by Dependency Injection mechanisms.

    There are debates about whether persistence logic should be put directly into the Rich Model, but in this article we'll be keeping only the business logic there.

  • Anemic Model

    An approach that suggests keeping the logic in Services or other stateless classes. The Entities become just structures that group data together but they don't implement any of the business logic. This architecture is opposite to Rich Model.

    Read the original post by Fowler.

  • Mocks are not always bad

    Note, that mocking can still be valuable when talking about system boundaries - testing an application with real integrations is often too complicated and should be limited to a small suite. The rest of the tests that require external integrations should use mocks.

    But to mock external systems it's better to write mocked classes - mocking frameworks will only add duplication and thus will complicate the overall testing. But this deserves an article of its own.

  • About code coverage

    You're probably familiar with Line and Branch coverage, but Code Coverage is a broader term that describes how good/bad your tests check production code. In this article if you see "code coverage" it means an abstract, ultimate coverage - not a particular kind of it.

    Line/Branch coverage are popular not because they are good metrics, but because they are easy to measure. In reality you cannot rely on these types of coverage if they are used by themselves. They can tell you which parts of the code are not covered completely, but they fail to tell which parts are covered and what's the quality of the coverage. If you decide to keep track of these metrics, at least introduce mutation testing as well.

X

Title