Chapter 4. Unit Testing – Focusing on What You Do and Not on What Has Been Done

 

"To create something exceptional, your mindset must be relentlessly focused on the smallest detail."

 
 --Giorgio Armani

As promised, each chapter will explore a different Java testing framework and this one is no exception. We'll use TestNG to build our specifications.

In the previous chapter, we practiced the red-green-refactor procedure. We used unit tests without going deeper into how unit testing works in the context of TDD. We'll build on the knowledge from the last chapter and go into more detail by trying to explain what unit tests really are and how they fit in to the TDD approach to build software.

The goal of this chapter is to learn how to focus on the unit we're currently working on and how to ignore or isolate those that were done before.

Once we're comfortable with TestNG and unit testing, we'll dive right into the requirements of our next application and start coding.

The following topics will be covered in this chapter:

  • Unit testing
  • Unit testing with TDD
  • TestNG
  • Remote-controlled ship requirements
  • Developing the remote-controlled ship
  • Summary

Unit testing

Frequent manual testing is too impractical to any but the smallest systems. The only way around this is the usage of automated tests. They are the only effective method to reduce the time and cost of building, deploying, and maintaining applications. In order to effectively manage applications, it is of the utmost importance that both the implementation and test code are as simple as possible. Simplicity is one of the core Extreme Programming (XP) values (http://www.extremeprogramming.org/rules/simple.html) and the key to TDD and programming in general. It is most often accomplished through division into small units. In Java, units are methods. Being the smallest, feedback loop they provide is the fastest so we spend most of our time thinking and working on them. As a counterpart to implementation methods, unit tests should constitute by far the biggest percentage of all tests.

What is unit testing?

Unit testing (UT) is a practice that forces us to test small, individual and isolated units of code. They are usually methods even though in some cases classes or even whole applications can be considered units, as well. In order to write UT, code under tests needs to be isolated from the rest of the application. Preferably, that isolation is already ingrained in the code or it can be accomplished with the usage of mocks (more on mocks will be covered in Chapter 6, Mocking - Removing External Dependencies). If unit tests of a particular method cross the boundaries of that unit, then they become integration tests. As such, it becomes less clear what is under tests. In case of a failure, the scope of a problem suddenly increases and finding the cause becomes more tedious.

Why unit testing?

A common question, especially within organizations that rely heavily on manual testing, is why should we use unit instead of functional and integration testing? This question in itself is flawed. Unit testing does not replace other types of testing. Instead, unit testing reduces the scope of other types of tests. By its nature, unit tests are easier and faster to write than any other type of tests, thus, reducing the cost and time-to-market. Due to the reduced time to write and run them, they tend to detect problems much sooner. The faster we detect problems, the cheaper it is to fix them. A bug that was detected minutes after it was created is much easier to fix than if that same bug was found days, weeks, or even months after it was made.

Code refactoring

Code refactoring is the process of changing the structure of an existing code without changing its external behavior. The purpose of refactoring is to improve an existing code. This improvement can be for many different reasons. We might want to make the code more readable, less complex, easier to maintain, cheaper to extend, and so on. No matter what the reason for refactoring is, the ultimate goal is always to make it better in one way or another. The effect of this goal is a reduction in technical debt; a reduction in pending work that needs to be done due to suboptimal design, architecture, or coding.

Typically, we approach refactoring by applying a set of small changes without modifying intended behavior. Reducing the scope of refactoring changes, allows us to continuously confirm that those changes did not break any existing functionality. The only way to effectively obtain this confirmation is through the usage of automated tests.

One of the great benefits of unit tests is that they are the best refactoring enablers. Refactoring is too risky when there are no automated tests to confirm that the application still behaves as expected. While any type of tests can be used to provide code coverage required for refactoring, in most cases only unit tests can provide the required level of details.

Why not use unit tests exclusively?

At this moment, you might be wondering whether unit testing could provide a solution for all your testing needs. Unfortunately, that is not the case. While unit tests usually cover the biggest percentage of your testing needs, functional and integration tests should be an integral part of your testing toolbox.

We'll cover other types of tests in more detail in later chapters. For now, a few important distinctions between them are as follows:

  • Unit tests try to verify small units of functionality. In the Java world, those units are methods. All external dependencies such as invocations of other classes and methods or database calls should be done in memory with the usage of mocks, stubs, spies, fakes, and dummies. Gerard Meszaros coined a more general term "test doubles" that envelops all those (http://en.wikipedia.org/wiki/Test_double).

    Unit tests are simple, easy to write, and fast to run. They are usually the biggest percentage of a testing suite.

  • Functional and acceptance tests have a job to verify that the application we're building works as expected, as a whole. While those two differ in their purpose, both share a similar goal. Unlike unit tests that are verifying the internal quality of the code, functional and acceptance tests are trying to ensure that the system is working correctly from the customer's or user's point of view. Those tests are usually smaller in number when compared with unit tests due to the cost and effort needed to both write and run them.
  • Integration tests intend to verify that separate units, modules, applications, or even whole systems are properly integrated with each other. You might have a frontend application that uses backend APIs that, in turn, communicate with a database. The job of integration tests would be to verify that all three of those separate components of the system are indeed integrated and can communicate with each other. Since we already know that all the units are working and all functional and acceptance tests are passed, integration tests are usually the smallest of all three as their job is only to confirm that all the pieces are working well together.
    Why not use unit tests exclusively?

The testing pyramid states that you should have many more unit tests than higher level tests (UI tests, integration tests, and so on). Why is that? Unit tests are much cheaper to write, faster to run, and, at the same time, provide much bigger coverage. Take, for example, registration functionality. We should test what happens when a username is empty, when a password is empty, when a username or password is not in the correct format, when the user already exists, and so on. Only for this single functionality there can be tens, if not hundreds of tests. Writing and running all those tests from the UI can be very expensive (time-consuming to write and slow to run). On the other hand, unit testing a method that does this validation is easy, fast to write, and fast to run. If all those cases are covered with unit tests, we could be satisfied with a single integration test that checks whether our UI is calling the correct method on the backend. If it is, details are irrelevant from an integration point of view since we know that all cases are already covered on the unit level.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.75.70