Chapter 7. Testing Web Applications

We are pretty sure you have heard the term "bug" when speaking about applications. Sentences such as "We found a bug in the application that…" followed by some very undesirable behavior are more common than you think. Writing code is not the only task of a developer; testing it is crucial too. You should not release a version of your application that has not been tested. However, could you imagine having to test your entire application every time you change a line? It would be a nightmare!

Well, we are not the first ones to have this issue, so, luckily enough, developers have already found a pretty good solution to this problem. In fact, they found more than one solution, turning testing into a very hot topic of discussion. Even being a test developer has become quite a common role. In this chapter, we will introduce you to one of the approaches of testing your code: unit tests.

In this chapter, you will learn about:

  • How unit tests work
  • Configuring PHPUnit to test your code
  • Writing tests with assertions, data providers, and mocks
  • Good and bad practices when writing unit tests

The necessity for tests

When you work on a project, chances are that you are not the only developer who will work with this code. Even in the case where you are the only one who will ever change it, if you do this a few weeks after creating it, you will probably not remember all the places that this piece of code is affected. Okay, let's assume that you are the only developer and your memory is beyond limits; would you be able to verify that a change on a frequently used object, such as a request, will always work as expected? More importantly, would you like to do it every single time you make a tiny change?

Types of tests

While writing your application, making changes to the existing code, or adding new features, it is very important to get good feedback. How do you know that the feedback you get is good enough? It should accomplish the AEIOU principles:

  • Automatic: Getting the feedback should be as painless as possible. Getting it by running just one command is always preferable to having to test your application manually.
  • Extensive: We should be able to cover as many use cases as possible, including edge cases that are difficult to foresee when writing code.
  • Immediate: You should get it as soon as possible. This means that the feedback that you get just after introducing a change is way better than the feedback that you get after your code is in production.
  • Open: The results should be transparent, and also, the tests should give us insight to other developers as to how to integrate or operate with the code.
  • Useful: It should answer questions such as "Will this change work?", "Will it break the application unexpectedly?", or "Is there any edge case that does not work properly?".

So, even though the concept is quite weird at the beginning, the best way to test your code is… with more code. Exactly! We will write code with the goal of testing the code of our application. Why? Well, it is the best way we know to satisfy all the AEIU principles, and it has the following advantages:

  • We can execute the tests by just running one command from our command line or even from our favorite IDE. There is no need to manually test your application via a browser continually.
  • We need to write the test just once. At the beginning, it may be a bit painful, but once the code is written, you will not need to repeat it again and again. This means that after some work, we will be able to test every single case effortlessly. If we had to test it manually, along with all the use cases and edge cases, it would be a nightmare.
  • You do not need to have the whole application working in order to know whether your code works. Imagine that you are writing your router: in order to know whether it works, you will have to wait until your application works in a browser. Instead, you can write your tests and run them as soon as you finish your class.
  • When writing your tests, you will be provided with feedback on what is failing. This is very useful to know when a specific function of the router does not work and the reason for the failure, which is better than getting a 500 error on our browser.

We hope that by now we have sold you on the idea that writing tests is indispensable. This was the easy part, though. The problem is that we know several different approaches. Do we write tests that test the entire application or tests that test specific parts? Do we isolate the tested area from the rest? Do we want to interact with the database or with other external resources while testing? Depending on your answers, you will decide on which type of tests you want to write. Let's discuss the three main approaches that developers agree with:

  • Unit tests: These are tests that have a very focused scope. Their aim is to test a single class or method, isolating them from the rest of code. Take your Sale domain class as an example: it has some logic regarding the addition of books, right? A unit test might just instantiate a new sale, add books to the object, and verify that the array of books is valid. Unit tests are super fast due to their reduced scope, so you can have several different scenarios of the same functionality easily, covering all the edge cases you can imagine. They are also isolated, which means that we will not care too much about how all the pieces of our application are integrated. Instead, we will make sure that each piece works perfectly fine.
  • Integration tests: These are tests with a wider scope. Their aim is to verify that all the pieces of your application work together, so their scope is not limited to a class or function but rather includes a set of classes or the whole application. There is still some isolation in case we do not want to use a real database or depend on some other external web service. An example in our application would be to simulate a Request object, send it to the router, and verify that the response is as expected.
  • Acceptance tests: These are tests with an even wider scope. They try to test a whole functionality from the user's point of view. In web applications, this means that we can launch a browser and simulate the clicks that the user would make, asserting the response in the browser each time. And yes, all of this through code! These tests are slower to run, as you can imagine, because their scope is larger and working with a browser slows them down quite a lot too.

So, with all these types of tests, which one should you write? The answer is all of them. The trick is to know when and how many of each type you should write. One good approach is to write a lot of unit tests, covering absolutely everything in your code, then writing fewer integration tests to make sure that all the components of your application work together, and finally writing acceptance tests but testing only the main flows of your application. The following test pyramid represents this idea:

Types of tests

The reason is simple: your real feedback will come from your unit tests. They will tell you if you messed up something with your changes as soon as you finish writing them because executing unit tests is easy and fast. Once you know that all your classes and functions behave as expected, you need to verify that they can work together. However, for this, you do not need to test all the edge cases again; you already did this when writing unit tests. Here, you need to write just a few integration tests that confirm that all the pieces communicate properly. Finally, to make sure that not only that the code works but also the user experience is the desired one, we will write acceptance tests that emulate a user going through the different views. Here, tests are very slow and only possible once the flow is complete, so the feedback comes later. We will add acceptance tests to make sure that the main flows work, but we do not need to test every single scenario as we already did this with integration and unit tests.

Unit tests and code coverage

Now that you know what tests are, why we need them, and which types of tests we have, we will focus the rest of the chapter on writing good unit tests as they will be the ones that will occupy most of your time.

As we explained before, the idea of a unit test is to make sure that a piece of code, usually a class or method, works as expected. As the amount of code that a method contains should be small, running the test should take almost no time. Taking advantage of this, we will run several tests, trying to cover as many use cases as possible.

If this is not the first time you've heard about unit tests, you might know the concept of code coverage. This concept refers to the amount of code that our tests execute, that is, the percentage of tested code. For example, if your application has 10,000 lines and your tests test a total of 7,500 lines, your code coverage is 75%. There are tools that show marks on your code to indicate whether a certain line is tested or not, which is very useful in order to identify which parts of your application are not tested and thus warn you that it is more dangerous to change them.

However, code coverage is a double-edge sword. Why is this so? This is because developers tend to get obsessed with code coverage, aiming for a 100% coverage. However, you should be aware that code coverage is just a consequence, not your goal. Your goal is to write unit tests that verify all the use cases of certain pieces of code in order to make you feel safer each time that you have to change this code. This means that for a given method, it might not be enough to write one test because the same line with different input values may behave differently. However, if your focus was on code coverage, writing one test would satisfy it, and you might not need to write any more tests.

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

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