Book Image

C# and .NET Core Test Driven Development

By : Ayobami Adewole
Book Image

C# and .NET Core Test Driven Development

By: Ayobami Adewole

Overview of this book

This book guides developers to create robust, production-ready C# 7 and .NET Core applications through the practice of test-driven development process. In C# and .NET Core Test-Driven Development, you will learn the different stages of the TDD life cycle, basics of TDD, best practices, and anti-patterns. It will teach you how to create an ASP.NET Core MVC sample application, write testable code with SOLID principles and set up a dependency injection for your sample application. Next, you will learn the xUnit testing framework and learn how to use its attributes and assertions. You’ll see how to create data-driven unit tests and mock dependencies in your code. You will understand the difference between running and debugging your tests on .NET Core on LINUX versus Windows and Visual Studio. As you move forward, you will be able to create a healthy continuous integration process for your sample application using GitHub, TeamCity, Cake, and Microsoft VSTS. By the end of this book, you will have learned how to write clean and robust code through the effective practice of TDD, set up CI build steps to test and build applications as well as how to package application for deployment on NuGet.
Table of Contents (11 chapters)

The principles of test-driven development

Test-driven development (TDD) is an iterative agile development technique that emphasizes test-first development, which implies that you write a test before you write production-ready code to make the test pass. The TDD technique focuses on writing clean and quality code by ensuring that the code passes the earlier written tests by continuously refactoring the code.

TDD, being a test-first development approach, places greater emphasis on building well-tested software applications. This allows developers to write code in relation to solving the tasks defined in the tests after a thorough thought process. It is a common practice in TDD that the development process begins with writing the tests code before the actual application code is written.

TDD introduces an entirely new development paradigm and shifts your mindset to begin thinking about testing your code right before you even start writing the code. This contrasts with the traditional development technique of deferring code testing to the later stage of the development cycle, an approach known as test last development (TLD).

TDD has been discussed at several conferences and hackathons. Many technology advocates and bloggers have blogged about TDD, its principles, and its benefits. At the same time, there have been many talks and articles written against TDD. The honest truth is TDD rocks, it works, and it offers great benefits when practiced correctly and consistently.

You might probably be wondering, like every developer new to TDD, why write a test first, since you trust your coding instinct to write clean code that always works and usually will test the entire code when you've done coding. Your coding instinct may be right or it may not. There is no way to validate this assumption until the code is validated against a set of written test cases and passes; trust is good, but control is better.

Test cases in TDD are prepared with the aid of user stories or use cases of the software application being developed. The code is then written and refactored iteratively until the tests pass. For example, a method written to validate the length of a credit card might contain test cases to validate the correct length, incorrect length, and even when the null or empty credit card is passed as a parameter to the method.

Many variants of TDD have been proposed ever since it was originally popularized. A variant is behavior-driven development (BDD) or acceptance test–driven development (ATDD), which follows all the principles of TDD while the tests are based on expected user-specified behavior.

Origin of TDD

There is literally no written evidence as to when the practice of TDD was introduced into computer programming or by which company it was first used. Nevertheless, there is an excerpt from Digital Computer Programming, by D.D. McCracken, in 1957, which indicated that the concept of TDD was not new and had been used by earlier folks, though the nomenclature apparently was different.

The first attack on the checkout problem may be made before coding has begun. In order to fully ascertain the accuracy of the answers, it is necessary to have a hand-calculated check case with which to compare the answers which will later be calculated by the machine. This means that stored program machines are never used for a true one-shot problem. There must always be an element of iteration to make it pay.

Also, in the early 1960s, folks at IBM ran a project (Project Mecury) for NASA where they utilized a technique like TDD where half-day iterations were done and the development team performed a review of the changes made. This was a manual process and cannot be compared to the automated tests we have today.

TDD was originally popularized by Kent Beck. He attributed it to an excerpt he read in an ancient book where TDD was described with the simple statements, you take the input tape, manually type in the output tape you expect, then program until the actual output tape matches the expected output. The concept of TDD was redefined by Kent Beck when he developed the first xUnit test framework at Smalltalk.

It is safe to say that the Smalltalk community used TDD long before it became widespread because SUnit was used in the community. Not until SUnit was ported to JUnit by Kent Beck and other enthusiasts was it that TDD became widely known. Since then different testing frameworks have been developed. A popular tool is the xUnit, with ports available for a large number of programming languages.

TDD misconceptions

Developers have different opinions when it comes to TDD. Most developers do complain about the time and resources required to practice TDD fully and how practicing TDD might not be feasible, based on tight deadlines and schedules. This perception is common among developers just adopting the technique, on the premise that TDD requires writing double code and that time spent doing this could have been used to work on developing other features, and that TDD is best suited for projects with small features or tasks and will be time-wasting with little return on investment for large projects.

Also, some developers complain that mocking can make TDD very difficult and frustrating, as the required dependencies are not to be implemented at the same time the dependent code is being implemented but should be mocked. Using the traditional approach of testing last, the dependencies can be implemented and all the different parts of the code can be tested afterwards.

Another popular misconception is that in the real sense tests cannot be written until the design is determined which relies on code implementation. This is not true, as adopting TDD will ensure there is a clear-cut plan on how the code implementation is to be done, which in turn gives a proper design which can aid the creation of efficient and reliable tests for the intended code to be written.

Some folks at times use TDD and unit testing interchangeably, taking them to be the same. TDD and unit testing are not the same. Unit testing involves practicing TDD at the smallest unit or level of coding, which is a method or function, while TDD is a technique and design approach that encompasses unit testing and integration testing, as well as acceptance testing.

Developers new to TDD often think you must completely write the tests before writing the actual code. The reverse is the case as TDD is an iterative technique. TDD favors exploratory processes where you write the tests and you write enough code. If it fails, you refactor the code until it passes and you can move on to implementing the next feature of your application.

TDD is not a silver bullet that automatically fixes all your bad coding behaviors. You can practice TDD and still write bad code or even bad tests. This is possible if the TDD principles and practices are not correctly used, or even when trying to use TDD where it's not practical to use it.

Benefits of TDD

TDD, when done correctly and appropriately, can give a good return on investment as it facilitates the development of self-testing code, which yields robust software applications with fewer or no bugs. This is because most of the bugs and issues that might appear in production would have been caught and fixed during the development stage.

Documenting the source code is a good coding practice, but in addition to source code documentation, tests are miniature documentations of the source code as they serve as a quick way to understand how a piece of code works. The test will show the expected input together with the expected output or outcomes. The structure of an application can be easily understood from the tests, as there will be tests for all the objects as well as tests for the methods of the objects, showing their usage.

Practicing TDD correctly and continuously helps you to write elegant code with good abstraction, flexible design, and architecture. This is true because, to effectively test all parts of an application, the various dependencies need to be broken down into components that can be tested in isolation and later tested when integrated.

What makes a code clean is when the code has been written using best industry standards, can be easily maintained, is readable, and has tests written to validate its consistent behavior appropriately . This indicates that a code without testing is a bad code as there is no specific way of directly verifying its integrity.

Types of tests

Testing software projects can take different forms and is often carried out by the developers and test analysts or specialists. Testing is carried out to ascertain that the software meets its specified expectation, to identify errors if possible, and to validate that the software is usable. Most programmers often take testing and debugging to be the same. Debugging is carried out to diagnose errors and issues in software and take the possible corrective measures.

Unit tests

This is a level of testing that involves testing each unit that constitutes the components of a software application. This is the lowest level of test and it is done at the method or function level. It is primarily done by programmers, specifically to show code correctness and that the requirement has been correctly implemented. A unit test usually has one or more inputs and outputs.

It is the first level of test usually done in software development and it is designed to isolate units of software systems and test them independently or in isolation. Through unit testing, inherent issues and bugs in systems can be easily detected earlier in the development process.

Integration tests

An integration test is done by combining and testing different units or components that must have been tested in isolation. This test is to ensure that the different units of an application can work together to satisfy the user requirements. Through integration tests, you can uncover bugs in the system when different components interact and exchange data.

This test can be carried out by programmers, software testers, or quality assurance analysts. There are different approaches that can be used for integration testing:

  • Top down: Top-level components are integrated and tested first before the lower level components
  • Bottom up: Lower-level components are integrated and tested before top level components
  • Big bang: All components are integrated together and tested at once

System testing

This level of test is where you validate the entire integrated system to ensure it complies with the specified user requirements. This test is usually performed immediately after the integration test and is carried out by dedicated testers or quality assurance analysts.

The whole software system suite is tested from the user's perspective to identify hidden issues or bugs and usability problems. A rigorous testing of the implemented system is done with the real inputs that the system is meant to process and output is validated against the expected data.

User acceptance testing

User acceptance tests are usually written to specify how software applications work. These tests are intended for business users and programmers and are used to determine if the system meets the expectations and user-specific requirements, and whether the system has been developed completely and correctly based on the specifications. This test is conducted by end users in collaboration with the system developers to determine whether to accept the system formally or make adjustments or modifications.

Principles of TDD

The practice of TDD helps with the design of clean code and serves as a buffer against regression in a large code base. It allows developers to determine easily whether newly implemented features have broken other features that were previously working through the instant feedback obtainable when the tests are run. The working principles of TDD are explained in the following diagram:

Writing the tests

This is the initial step of the technique, where you have to write the tests that describe a component or feature to be developed. The component can be the user interface, business rule or logic, data persistence routine, or a method implementing a specific user requirement. The tests need to be brief and should contain the required data input and desired outcome expected by the component being tested.

While writing the tests, technically you have solved half of the development task, because the design of the code is usually conceived through the thought pattern and process put into writing the tests. It becomes easier to tackle the difficult code after the easier code, which is the test that has been written. At this point, as a TDD newcomer, the tests are not expected to be 100% perfect or have full code coverage, but with continuous practice and adequate refactoring, this can be achieved.

Writing the code

After the tests have been written, you should write enough code to implement the feature for the tests you wrote earlier. Bear in mind that the goal here is to try to employ good practices and standards in writing the code to make the test pass. All the approaches that lead to writing bad or stinking code should be avoided.

Try to avoid test overfitting, a situation where you write code just to make the tests pass. Instead you should write the code to implement the feature or user requirements fully, so as to ensure that every possible use case of the feature is covered to avoid situations where the code has different behaviors when executed by the test cases and when in production.

Running the tests

When you are sure you have enough code to make the test pass, you should run the test, using the test suite of your choice. At this point, the test might pass or fail. This depends on how you have written the code.

A thumb rule of TDD is to run the tests several times until the tests pass. Initially, when you run the test before the code is fully implemented, the test will fail, which is the expected behavior.

Refactoring

To achieve full code coverage, both the tests and the source code have to be refactored and tested several times to ensure that a robust and clean code is written. Refactoring should be iterative until full coverage is achieved. The refactoring step should remove duplicates from code and attempt to fix any signs of code smell.

The essence of TDD is to write clean code and in turn solid applications, depending on the type of tests being written (unit, acceptance, or integration tests). Refactoring can be localized to just a method or it can affect multiple classes. When refactoring, for example, an interface or multiple methods in a class, it is recommended you make the changes gradually, taking it one test at a time until all the tests and their implementation code are refactored.

Doing TDD the wrong way

As interesting as practicing TDD can be, it can also be wrongly done. Programmers new to TDD can sometimes write monster tests that are way too large and defeat the purpose of test brevity and being able to perform the TDD cycle quickly, leading to a waste of productive development time.

Partial adoption of the technique can also reduce the full benefit of TDD. In situations where only a few developers in a team use the technique and others don't, this will lead to fragmented code where a portion of code is tested and another portion is not, resulting in an unreliable application.

You should avoid writing tests for code that are naturally trivial or not required; for example, writing tests for object accessors. Tests should be run frequently, especially through the use of test runners, build tools, or continuous integration tools. Failing to run the tests often can lead to a situation where the true reflection of the state of the code base is not known even when changes have been made and components are probably failing.