Untangling concepts: Unit Tests vs Acceptance Tests

Today I'd like to introduce a new serie of posts named Untangling Concepts. In the IT business we are overloaded day by day with more and more information and sometimes it's difficult to handle all this knowledge without making mistakes. The main goal of these posts is to clarify the difference between concepts that sometimes are mixed together.

In the first post I'll write about unit testing and acceptance testing. I'd like to emphasize that this post isn't about Test Driven Development. TDD is a subject to other posts.

Unit Tests

When we talk about unit tests the first thing that should come to mind is the Single Responsibility Principle. The name unit tests isn't a coincidence, you should test one small piece of code which does just one thing. I guarantee that if you code big classes with big methods which do a lot of stuff, you'll have trouble to write good and useful unit tests. High cohesion matters.

So, what is the point in unit tests? Imagine the design of a system. If you follow the object oriented software design good practices you will come up to a system where a lot of objects communicate with each other in order to achieve the program's goal.

Imagine the following situation: you have to implement an e-mail system. The program receives as input a list of e-mail addresses and have to send an e-mail to each one of the recipients. You might have something like this:

In this case, someone will call the readRecipientList method with an InputStream (e.g. a FileInputStream of a txt file with some e-mail addresses). After that, the method sendMails is called, returning the number of e-mails sent.

But what should happen if the method sendMails is called before the method readRecipientList is called? And what if I pass a FileInputStream of an empty file? Or even a null InputStream object? How can I trust that the class MailSender is doing it's job right?

Those are the type of question that unit tests try to answer. When writing unit tests, our concern is to assert that our objects' internal structure is working as expected. It's like a puzzle... If I can assert that each piece is working as expected, I can trust that when the pieces are put together (in the right places... more about that in the next session), the puzzle will be completed correctly.

And that's not everything. What if you have to change something inside one piece of code, or if you need to do some refactoring (you should do it a lot)? If you have tests to assert that this piece of code is working as expected you can run these tests after the changes and see if your modifications broke something. You can make changes more confidently.

There are a lot to say about unit tests and how they can be beneficial to your code. Here I just want that you understand the concept, but I'll put some links in the Useful Resources section so you can read a lot more about that.

Acceptance Tests

Now let's talk about acceptance tests. Put yourself in the place of a stakeholder. You know what you want the software to do but you know nothing about software development nor programming. How can you believe that the software is doing everything you want, in the way you want?

Unit tests are good to test small pieces of code and to assert that the code works as expected. But that isn't enough to be sure that the code is satisfying the stakeholders.

Acceptance tests are test cases written based on scenarios specified by the customer. Usually each history will have at least one associated acceptance test. They work as black box tests, meaning that shouldn't be considered implementation details in those tests (one more reason to write them first...). It's like an input-output evaluation:

"Given some context, When something happen, Then I expect the system to answer that (or to be in that state)."

We use this Given-When-Then template to write good acceptance tests for a user history.

Let's try a little example. Remember our e-mail system? Which scenario do you think that our customer would like to satisfy? I believe that the most obvious is something like that:

Given that I am logged in the mail system, When I send an e-mail to someone@company.com, Then I expect that there is one new mail in someone's inbox.

This is just one super basic example. It's not unusual to have acceptance tests with far more Given and Then clauses.

The best way to write acceptance tests is using something that everyone can understand. But that's not easy. We can use, for example, Selenium to capture tests based in user's input, but that depends in having at least a prototype interface where the user can navigate. In the last project I have been working on, we have successfully adopted acceptance tests written in Gherkin, a language used by Cucumber.

For example, a more detailed version of our test of sending an e-mail would look like that in Gherkin:

Feature: Sending an e-mail to someone

Scenario: Send an e-mail to an existing user

Given that I am authenticated in the Mail system
 And that exists an account: someone@company.com
When I send an e-mail to someone@company.com
Then someone's Inbox must have one unread mail
 And my Sent Itens folder must have the sent mail

As you can see, we are writing our tests in plain english. Anyone can read them and understand what the system is supposed to do (the bold words have nothing to do with Gherkin, it's a bug in the syntax highlight feature).

I see two great benefits in writing acceptance tests before the development of a history. The first one is that we can be sure that the customer knows what he wants to be developed and that the developer knows what the customer wants. So we reduce the chance of misunderstandings, increasing customer satisfaction. The second is that we have an easy feedback when any of the user histories are broken during the development cycle. The more we run the acceptance tests, faster we can see if our system is not doing what it is supposed to do. And faster we fix it.

Conclusion

In this article we learned that unit tests are the best tool that developers have to protect themselves from mistakes. The wider our test coverage is, more we can do refactoring with confidence, leading to better code. Remember, code rots.

Acceptance tests is an excelent tool to help to make the gap between the customer desires and the developer understanding smaller. They help in the fast feedback loop, leading to software that makes our customer happier.

Remember that unit tests and acceptance tests complement each other. Our goal is not just to build the right thing but make sure that we build the thing right.

Useful Resources