Friday, November 1, 2013

The Pyramid of Tests

Recently I’ve been talking a lot about testing, all kind of testing. It surprised me that most of the people don’t know what kind of tests are there (Yes! It’s not just unit tests and integration tests).
The obvious
I hope that you already know that if your code’s build is successful that doesn’t necessary mean that it works. You’ve got to test it!
- How do we do that?
- We unit test!
That is correct. However, just unit testing your code is not enough. In this post I will describe all the six different types of tests.
Unit Test
A unit test tests a “unit of work” without relying on its dependencies.
   1: public class BankAccount

   2: {

   3:     public double Balance { get; set; }


   5:     public bool TryWithdrawMoney(double amount)

   6:     {

   7:         if (Balance > amount)

   8:         {

   9:             Balance = - amount;

  10:             return true;

  11:         }


  13:         return false;

  14:     }

  15: }

The BankAccount entity is independent so unit testing the “TryWithdrawMoney” is quite simple.

   1: public class BankAccountInformationService

   2: {

   3:     private readonly IAuthorizeUser _userAuthorization;

   4:     private readonly ISession _session;

   5:     private readonly ILogger _log;


   7:     public BankAccountInformationService(IAuthorizeUser userAuthorization, ISession session, ILogger log)

   8:     {

   9:         _userAuthorization = userAuthorization;

  10:         _session = session;

  11:         _log = log;

  12:     }


  14:     public BankOperationResult WithdrawMoney(int accountNumber, double amount)

  15:     {

  16:         if (_userAuthorization.IsAccountFrozen(accountNumber))

  17:             return new BankOperationResult { Success = false, Message = "Cannot perform operations on a frozen account" };


  19:         var userBankAccount = _session.Load<BankAccount>(accountNumber);


  21:         _log.Write(string.Format("Attempt to withdraw {0} NIS from account number {1}", amount, accountNumber));


  23:         var withdrawSucceeded = userBankAccount.TryWithdrawMoney(amount);


  25:         return new BankOperationResult { Success =  withdrawSucceeded};

  26:     }

  27: }

The BankAccountInformationService on the other hand has some dependencies that are part of the unit of work. In order to test the class we will mock them.
- Bug types: business logic bugs.
So far so good. But is it enough?
Can I rely on my NHibernate mappings? Or my logger? Or that “userAuthorization” dependency? All of above aren’t really ordinary classes as the BankAccount – the mappings work against a DB, the logger writes to file system or wherever, the authorization thing works against Active Directory. If I mock their real behavior (mock a real insert to the DB) I won’t really test anything.
Bugs can be found in every single piece of code that I write! The mappings can be incorrect (wrong cascade usage, uniqueness etc), the logger doesn’t actually write wherever it supposed to and the active directory access implementation is simply incorrect. By unit testing these components I won’t be able to find the bugs they potentially have.
Component Tests
NHibernate is a component in my application to test it I will actually insert a mapped entity to the DB, get it back and check that all the properties were mapped correctly. Same thing with any other component I have – let them do their actual job and test it.
Notice that although these type of tests access the DB or the file system they are called component tests and not integration (we’ll get there).
- Bug types: different component access/usage bugs.
So we tested the components and unit tests our class? Is this enough? The answer is still no. Despite the fact we tested each class independently we cannot assume that they collaborate together. We need to make sure that our whole system works. We want to check that our service uses its dependencies correctly and handles their real (not mocked) behavior as expected.
System Tests
No mocking included! This is the real deal. I want to verify that my service which I expose to the world actually works so I call it with real parameters and test that it actually does what it supposed to. The challenge with these tests is to create the proper environment for the test and clean all the test data after the test.
By the way these tests can later be used as load tests.
- Bug types: Components collaboration bugs, loads failure.
Integration Tests
Integration is the collaboration of two systems. Meaning if we have a client that contributes my service an integration test will test how the client handles the service behavior.
- Bug types: communication faults, security issues and data contract mismatches.
UI Tests
After validation the integration of the client and the service it’s time to validate the client itself. You need to actually play with the client, click the buttons, enter input and validate the output and the UI logic (disable/enabled buttons etc). You need to create real test cases that match the real operations that your user will perform on the client. You can do it manually or automatically using coded UI tests.
- Bug types: UI logic bugs.
Smoke Tests
The last kind of tests is when you deploy your application and you want to verify that the installation was successful. Validate the DB schema was created, the IIS services are up and so on. These tests don’t check any business logic just that the different components of your application were installed correctly and that the application is ready for the user.
- Bug types: deployment configuration and installation bugs.
There you have it! The Pyramid of Tests:
If you ignore a step in your app then finding why a test has failed will take much longer as every test is responsible for different bug types, plus you won’t be able to test your application in cases that cannot be prepared like not enough space in the file system or a DB failure.
Be safe. Until next time.

No comments:

Post a Comment