Lesson 1 - Introduction to web application testing in PHP
Welcome, all advanced programmers, to the first lesson of an online course about testing PHP applications. The course is written upon practical experience with larger projects and we'll gradually learn to cover our code with different types of tests and thus create reliable and high quality applications. It's intended for all who strive for a decent employment, where you'll be very well rated for this knowledge, or for those, who have their own application and would like to extend it comfortably so you won't have to worry whether code changes broke the original functionality.
I'll assume you know:
- Basics of programming
- Object-oriented programming
- At least basics of software design (I'll use terms like "use case" here and similar, so to make us understand each other )
Testing is actually the fourth point of the outline above, that every good programmer should know for his work to be worth it.
When to test
Testing surely belongs among best software development practices. Another such a practice is, for example, object-oriented programming, using a multi-layered architecture and so on. We should be really orthodox with some of the practices, for example, there is only one reason to write a non-object-oriented code and it's the ignorance of the programmer. Encapsulating the application logic into objects and layers causes a minimal delay for experienced developers and minimizes the cost of expanding and maintaining unclear and non-layered applications. On the other hand, some software development practices shouldn't be followed in such extreme manners and testing belongs among these.
Let me mention straight from the start that testing is very important and in particular stages of a project even indispensable. On the other hand, in the early stages of the project (speaking especially about start-ups), the time is our enemy, the features of the application is chaning very often, and it's needed to release as soon as possible, it's not a good idea to write tests. Such tests would have to be changed often, would create an unnecessarily delay and could even harm the start-up. I might have triggered thoughts about the theory around the Test-Driven Development (TDD) in some of you, which is based on constant testing of everything. As a theoretical concept, it sounds nice, but in practice, a good programmer should be able to think at least a little like a manager and spend the development budget rationally. After all, even their salary is based on how competitive the application is. On the other hand, if you have an application that already has several stable features and you want to expand it further, you wouldn't be able to do it without tests. Therefore, we cover applications with tests mainly when these applications already have several stable features.
Expanding your sofware
To expand a software, if you do it well, always means to change its existing features. A good quality analysis and design can help to pave the way for future adjustments, but even if you do it all right, the market is changing uncontrollably and a successful app will always be edited plenty sooner or later. If you won't edit the existing features, redundant code will begin to appear (e.g. you'll write a similar method again instead of just parameterizing the one that the app already has, add unnecessary database tables instead of simply modifying the current data model and so on).
You probably suspect that avoiding to edit the existing application code is not worth it at all. Its design would suffer, and in the future, it'd be very difficult to modify or extend code "glued" together like this. You'd need to make changes in multiple places, there'd be redundant code blocks etc. We've come to the conclusion that you'll always be changing and rewriting your app, this is just what the software development is like. And who'll test that everything is still working later? A person? Since we've already came to an agreement that we have to test whether the previous functionality hasn't broken down, let's talk about why testing can't be done by a person.
Why can't a person do it?
Let's think about a naive idea.
Expectation
The programmer or tester sits down to the computer and goes through one feature after another and checks whether they all work. Take ICT.social as an example. The tester would try to sign up, sign in, change the forgotten password, edit the profile, buy premium points with a credit card... There're hundreds of features (use cases) in this system. If you think, that a person would test them for hours, and that's why we'll let a machine do it, which would probably do so in a few minutes, you're still not right. Sitting and clicking all day still isn't principally such a problem. Writing automatic tests takes quite a long time, it could still be worth it. But...
Reality
Imagine you're testing an application like this, you're in the middle of a test scenario and you've found an error. It's not possible to comment an article, for example, due to a validator change. You'll fix this error and continue testing successfully until the end of the scenario. You'll deploy the successfully tested application to the production server and the next day, you'll receive an email saying that it's not possible to buy articles. After a bit of research, you'll find that the fix you've done yesterday has broken another functionality you've tested before the fix. This could have easily cost you several hundreds of dollars before someone reported you this bug. And we didn't even mention the catastrophic scenarios of data leaks, accidentally spamming our users, and so on. If a fix is made during the testing phase, you must test the all the scenarios from the beginning! Since there're usually multiple errors found, the entire testing could take a few days of clicking. The programmer would probably not stand it and won't perform the tests carefully, causing the production errors (I've personally never been able to stand the frustration from keeping doing the same actions over and over again ).
This is why tests need to be done by a machine, which usually clicks through the entire in tens of minutes, and can do it over and over again. That's why we're writing automatic tests, unfortunately, most of the tutorials on the Internet won't tell you this.
Test types
Let's now focus on what to test in our applications. There are several types of tests, they usually don't cover all possible scenarios (all the code), but we're talking about percentage code coverage, mostly of the critical application parts. The bigger the application, the more test types it needs and the more functionality we usually cover. Early versions of smaller applications mostly don't need any tests or just the basic ones, for example, the to make sure users can register.
Let's describe the basic test types:
- Unit tests - These typically test universal libraries, we don't write them for application-specific code. Unit tests test classes, their methods to be accurate, one after another. They pass different inputs to the methods and test whether the outputs are correct. It doesn't make much sense to use unit tests to test whether a method performing a database query used in a single controller (or presenter, some control layer) returns what it should. On the contrary, it makes very much sense to test, for example, a phone number validator which we use at 20 different places or even in multiple applications. Such a unit test would test for example 20 different correct and incorrect phone numbers to test whether the validator really determines which ones are valid and which aren't. Unit tests are whitebox tests, that means we know how the tested code works inside when we write them (we can see inside).
- Acceptance tests - This type of tests is totally shielded from how the application is programmed inside, they're blackbox tests (we don't see inside). Each test typically tests a particular feature, for example, a test for writing articles would test the individual use cases associated with it (to submit an article for approval, to approve an article, to reject an article, to publish an article as the administrator...). These tests typically use the Selenium framework, which allows to automatically click through the web browser and simulate an Internet user who uses our application. These tests basically test the application specific logic (database queries, etc.), the result generated by our application, not by its internal code.
- Integration tests - Nowadays, applications are quite complex and often divided into several services that communicate with each other and which are developed separately. Integration tests ensure that everything fits into its place properly.
- System tests - Even if the app works well, there'll be certain requirements for it on the production environment we should be aware of, e.g. that it should manage to serve a thousand user requests at one time. We'd test this by a stress test which belongs among the system tests.
- And many more...
V-model
Let's end this introduction to the software testing with the V-model. It extends the infamous waterfall model of software development, which has the following stages:
- Requirements analysis
- High-level design
- Detailed specification
- Implementation
As you surely know, nowadays software isn't developed by performing these 4 phases one after another anymore, but iteratively, by doing these phases in short time intervals for different parts of our application. The V-model assigns the test phase (the type of tests we discussed above) for each of these phases.
We can see that the name of the V-model comes from its similarity with the shape of the letter V. After the implementation, we verify the detailed specification with unit tests, high-level design with integration tests, and whether all use cases work with acceptance tests. The V-model is sometimes even a few stages taller, if the application is really complex and requires more test types, e.g. system tests as well.
In the next lesson, Introduction to unit tests in PHP and PHPUnit installation, we'll learn how to program unit tests, which are the simplest tests verifying the functionality of internal libraries (units), and we usually begin with them when testing applications.