TDD tells that a test case should be written first, then the code should be written.
TDD is a cyclical process and includes three stages: RED, GREEN, REAFTOR. In the RED stage, a test case is written and executed. The test case fails. In the GREEN stage, first code is written for the test case to pass. The code doesn't need to be clean. The main goal is to write the simplest code for the test case to pass. The test case is run again. If the test is successful, the REFACTOR stage is started. At this stage, if the code can be refactored, it is done. The test case is run again. If the test case fails, the code is refactored. This cycle is continued until the code becomes desired.
Let's consider the cart example in the e-commerce system and simulate the scenario where the product is added to the cart. First, we write our test case. This is the RED stage. We add two different products to the cart, one from the first product and two from the second product. We expect the total amount of the cart to be 500 and the number of products in the cart to be 2.
We run our test case and see that it failed. Because we added the AddItem method to the Cart We run our test scenario and see it fail. Because we added the AddItem method to the Cart class, but we haven't written any code in it yet. At this stage, our goal is not to write code.
Now we on the GREEN stage. At this stage, we edit the AddItem method to pass the test case. Our goal here isn't to write best code. We write the fastest, simplest code necessary for the test case to pass.
Our test passed. We have reached the REFACTOR stage. At this stage, we clean and organize our code. We convert the TotalPrice property to the computed property. We convert the Items property to immutable. Finally, in the AddItem method we check if the argument is null. We run the test case and see that it passed.
If you pay attention, another test case appeared during the refactor stage. We expect an error to occur if a null value is passed to the AddItem method. That's why we write a second test case. In this test case, we pass a null value to the AddItem method and expect an error to occur.
Each test is designed to have three different phases executed sequentially. These phases are ARRANGE, ACT and ASSERT.
At the arrange stage, the necessary preparations are made for the test. The dependencies which is required for installation are prepared. In our example we are creating one cart object. We add a product to the cart.
In the act phase, the test is performed. In our example, we run the IncreaseItemQuantity method.
At the assert stage, the test result is verified. In our example, we check the total amount of the cart and the number of items.
Each test is designed to have four different phases executed sequentially. These phases are SETUP, EXERCISE, VERIFY and TEARDOWN.
Setup is called before each test case is executed. Necessary preparations are made at this phase as in the Arrange stage. If you are using nUnit, you can make your preparations in a method which is marked with the SetUp attribute. If you are using xUnit you can do it in the constructor method.
Teardown is called before each test case is terminated. Preparations are reset. If you are using nUnit, you can reset it in a method which is marked with the Teardown attribute. If you are using xUnit you can do it in the Dispose method.
Exercise and Verify phases are found in the test case. During the exercise phase, the test is performed. During the Verify phase, the test result is verified.
Global Setup is run once before test cases are run. If you are using nUnit, you can make your preparations in a method which is marked with the OneTimeSetUp attribute. If you are using xUnit you can create Fixture and do it in its constructor method.
Global Teardown is called before test cases are terminated. If you are using nUnit, you can reset it in a method which is marked with the OneTimeTearDown attribute. If you are using xUnit you can create Fixture and do it in its Dispose method.
There are certain naming conventions for test case naming.
The name of the tested method, the tested condition, and the expected behavior are used. The biggest disadvantage of this rule is that the test case method names are also edited when the method names are changed.
Example usage : DecreaseItem_ItemQuantityIsOne_RemoveItem
The expected behavior and the tested condition are used. It's pretty easy to read.
Example usage : Should_RemoveItem_When_DecreaseItem_If_ItemQuantityIsOne
The tested condition and the expected behavior are used. It's pretty easy to read.
Example usage : When_ItemQuantityIsOne_Expect_RemoveItem
As you can see, everything is quite understandable. What we need to pay attention is that we should apply the same rule in all test cases whichever rule we prefer.
Types of Software Testing
There are three different types of tests. Unit Test, Integration Test and E2E Test.
At the beginning of the article, I mentioned that the main purpose of TDD is to write code by writing test cases. That's why UnitTest is the most important test for TDD. The most important rule of UnitTest is testing by removing 3rd party dependencies. Let's say a coupon has a ForLoyal property. It is necessary to query whether the customer is loyal and apply the coupon. In other words, the coupon must be get from the coupon service first. Then, it is necessary to query the customer information from the customer service. These services are 3rd party dependencies. Because these services communicate with the database. But our main goal is to test whether the coupon is applied to the cart. Therefore, the services that communicate with the database are mocked. Dummy coupons suitable for our test case are returned from these mocked services. For example; An expired coupon can be returned. Or, a ForLoyal coupon can be returned and a non-loyal customer can be returned from the customer service.
In Integration Test, real testing is performed with 3rd party dependencies. The database is accessed and the coupon is queried. After the coupon is applied, it can be recorded in the database in the same way. For this reason, integration tests are more complex.
In E2E Test, the test API is run. Requests are sent to endpoints and results are checked. This is the most expensive test.
Code coverage is a metric that shows how much of the code has been verified in test cases. Coverage increases as scenarios in code are verified. The higher the coverage, the safer the application.
You can access the sample project from my Github address.