Targeted at programmers, but all are welcome. Using the PHP language as an example.
A few words to start, if you are interested in writing tests and know how to write PHP code, then you can count yourself as part of the few professionals who use test-driven development (TDD).
Code-based testing is quite a broad topic. This short user guide will briefly outline the basics of automated tests found at the bast of the testing pyramid (fig. 1) that should be written by the same people writing the code – by programmers. We will not be focusing on high-level testing, such as acceptance tests or end-to-end tests created by Quality Assurance (QA) teams.
This user guide is also not a tutorial on how to write tests from scratch. Setting up the testing environment, using mock objects and fakes, and preparing data structures using fake data is often heavily dependent on the framework used, and so will not be a part of this user guide.
We will focus exclusively on how to make tests easier to read and understand, assuming you already know how to write working tests.
If you are already experienced in writing tests and would like to increase their quality, you can skip the rest of this introduction and move on to the next chapter, as you would already know the practical benefits of utilizing TDD.
For those of you who are willing to learn a more professional approach to creating software, here are a few words on the benefits of implementing automated tests written by programmers.
At first glance, writing tests seems like more trouble than it’s worth. That may be true in terms of small applications with simple logic that won’t be developed further and are only created as temporary proof of concept. However, thinking in that manner could prove to be very deceptive if the application will be continuously developed. The increased complexity of the business logic and a design which allows for the application to be used for years on end does not account for the different development stages, implementing new changes, or providing updates.
Let’s start with updating your application. It’s not only about fixing existing bugs, or optimizing code or its infrastructure, as a lot of this is directly connected with the people working on the project. They tend to change, or the number of new programmers in the team grows. This results in the code being worked on by people other than its original authors and who are hesitant to make any changes. Having automated tests helps introduce the changes made by new team members.
This brings us to the next section: introducing changes and improvements. Thanks to properly written tests, it is much easier for us to bring about changes and identify potential problems early in the code writing process, allowing us to fix them before the application reaches the server. Additionally, finding errors happens autonomously, helping new programmers as well as the author of the system. After all, it often happens that you return to code that you haven’t worked on for a long time and forgot how everything works.
From the above, we learn that tests prove to be helpful, and they limit the costs of system regression. However, TDD does not rely on writing extra tests to an existing, already working system. It may happen that you are forced to do exactly just that if the author hasn’t done so already, but tests should be created alongside code when implementing TDD, if not be slightly ahead. Although they may appear to be just another workload to deal with, especially when talking about more convoluted business logic and complicated algorithms, but code-based testing actually increases the speed with which code is written that meets all the required criteria.
Well written tests are a helpful tool used to write code from scratch. They also decrease debugging time, and they speed up the process of planning a specific piece of code using the provided input parameters. This prompts programmers to think about how they will split their code into happy, failed, or error paths. In this way, programmers are forced to decouple code, increasing its readability.
Another advantage is assisting with refactoring code. Writing perfect code is not easy in the beginning. This could be due to a number of factors, such as close deadlines, a lack of required technical knowledge, or by implementing a poorly documented third-party API. We need to refactor this kind of code without altering its core functionality. Only properly written tests can guarantee that code will work as intended, while simultaneously making the refactoring process quicker and easier.
An additional benefit to having well-written tests is creating documentation that explains how a piece of software works and how to use it. Code like this documents itself which UniTree graphically visualizes, making the whole experience even more comfortable. It’s yet another aspect that helps ease programmers into the team who work on a given project and minimizes the cost of bringing them up to speed.
Let’s start with differentiating two common types of code-based tests, which are the base of the testing pyramid.
Let’s separate these tests into two groups: those that analyze a specific function of a piece of our code, and those that analyze an entire algorithm that could consist of multiple classes. The first group is made of “unit tests”, forming the base of the pyramid. The other group consists of “integration tests”, situated directly above unit tests in the pyramid.
What is the key difference? In short, unit tests should analyze only one function – a specific public method in a class without its dependencies. Potential objects or other public methods used by the class should be mocked in a way so that they are cut off from the rest of the code that is not being currently tested.
Here is an example of an integration and unit test checking the same method in a class:
An example of an integration test
/** * @test * @covers WpCategorySynchronizer::synchronize */ public function synchronize(): void { $synchronizer = $this->newSynchronizer(new ApiWordpress()); $result = $synchronizer ->synchronize(); $this->assertDatabaseHas('meals', [ 'external_source' => 'WORDPRESS', 'title' => 'How To Make Chicken Fajitas (Easy Fajita Recipe)', ]); }
An example of a unit test
/** * @test * @covers WpCategorySynchronizer::synchronize */ public function synchronize() { $recipe_stream = $this->mockRecipeStream(); $api_wp = \Mockery::mock(ApiWordpress::class); $api_wp->shouldReceive('startRecipeStream')->once()->andReturn($recipe_stream); $synchronizer = $this->newSynchronizer($api_wp); $result = $synchronizer->synchronize(); $this->assertInstanceOf(Report::class, $result); }
In an ideal world, every application should be fully covered in unit tests. This way of thinking ironically causes unit tests to not be prepared at all, as the expectations are far too irrational and unrealistic. In our opinion, code testers should consider using both integration tests (also known as end-to-end tests) and unit tests.
Unit tests should cover specific cases of business logic, while integration tests should look at “simple” system elements such as requests performing simple CRUD operations that do not contain any additional business logic. In these kinds of cases, it is much quicker to cover the whole application with automated tests that make sense. It’s also often possible to forego the testing of “tools” provided by the framework you are using, as long as these tools fulfill their purpose and already have their own tests prepared. For example, Object-relational mapping (ORM) and a method of downloading all the database records, or looking up a specific record by its unique ID.
Give yourself time to decide on clear boundaries as to where unit and integration tests should appear in your directory tree. This brings us to Part 2, to which I already invite you.
Andrzej Fenzel, the Author of UniTree.app