C# and .NET Core Test Driven Development

4 (2 reviews total)
By Ayobami Adewole
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Exploring Test-Driven Development

About 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.

Publication date:
May 2018
Publisher
Packt
Pages
300
ISBN
9781788292481

 

Chapter 1. Exploring Test-Driven Development

In order to craft robust, maintainable, and scalable software applications, software development teams and stakeholders must make certain important decisions early in the software development process. These decision makers must adopt the software industry's tested and proven best practices and standards throughout the different stages of software development.

The quality of software projects can quickly degrade when developers build the code base using development approaches, coding styles, and practices that automatically make source code rigid and difficult to maintain. This chapter points out the habits and practices that result in writing bad code and should therefore be avoided. The programming habits, development styles, and approaches that should be learned to facilitate writing clean and maintainable code are explained.

In this chapter, we will cover the following topics:

  • Difficulty in maintaining code
  • How bad code gets that way
  • What we can do to prevent bad code
  • The principles of test-driven development
  • The test-driven development cycle
 

Difficulty in maintaining code


There are two types of code—good code and bad code. The syntax of both types of code may be correct when compiled and running the code can give the expected results. However, bad code leads to serious issues when it comes to extending or even making little changes to the code, due to the way it was written.

When programmers write code using unprofessional approaches and styles, this often results in bad code. Also, the use of unreadable coding styles or formats as well as not properly and effectively testing code are all precursors to bad code. Code can be written poorly when programmers sacrifice professionalism in order to meet approaching deadlines and project milestones.

I have come across a few software projects that rapidly become legacy software projects that are abandoned because of incessant production bugs and the inability to incorporate change requests from users easily. This is because these software applications were deployed into production with a severe accumulation of technical debts through bad code written by software developers as a result of making poor design and development decisions and using programming styles known to cause future maintenance problems. 

Source code elements—methods, classes, comments, and other artifacts—should be easy to read, understand, debug, refactor, and extend if required by another developer other than the original developer; otherwise, bad code has already been written.

You know you have bad code when, extending or adding new features, you break the existing working features. This can also happen when there are portions of code that cannot be decoded or any changes to them will bring the system to a halt. Another reason for bad code is usually because of nonadherence to object-oriented and Don't Repeat Yourself (DRY) principles or wrong use of the principles.

Note

DRY is an important principle in programming, which aims at breaking down a system into small components. These components can easily be managed, maintained, and reused in order to avoid writing duplicate code and having different parts of code performing the same function.

 

How does bad code appear? 


Bad code doesn't just appear in a code base; programmers write bad code. Most of the time, bad code can be written because of any of the following reasons:

  • Use of wrong approaches by developers when writing code that is often attributed to tight coupling of components
  • Faulty program designs
  • Bad naming conventions for program elements and objects
  • Writing code that is not readable as well as having a code base without proper test cases, thus causing difficulty when there is a need to maintain the code base

Tight coupling

Most legacy software applications are known to be tightly coupled, with little or no flexibility and modularity. Tightly coupled software components lead to a rigid code base which can be difficult to modify, extend, and maintain. As most software applications evolve over time, big maintenance issues are created when components of applications are tightly coupled. This is due to the changes in requirements, user's business processes, and operations.

Third-party libraries and frameworks reduce development time and allow developers to concentrate on implementing users' business logic and requirements without having to waste valuable productive time reinventing the wheel through implementing common or mundane tasks. However, at times, developers tightly couple the applications with third-party libraries and frameworks, creating maintenance bottlenecks that require great efforts to fix when the need arises to replace a referenced library or framework.

The following code snippet shows an example of tight coupling with a third-party smpp library:

public void SendSMS()
{
    SmppManager smppManager= new SmppManager(); 
    smppManager.SendMessage("0802312345","Hello", "John");
}

public class SmppManager
{
    private string sourceAddress;
    private SmppClient smppClient;

    public SmppManager()
    {
       smppClient = new SmppClient();
       smppClient.Start();            
    }        

    public void SendMessage(string recipient, string message, string senderName)
    {
       // send message using referenced library            
    }    
}

Code smell

Code smell is a term that was first used by Kent Beck, which indicates deeper issues in the source code. Code smell in a code base can come from having replications in the source code, use of inconsistent or vague naming conventions and coding styles, creating methods with a long list of parameters, and having monster methods and classes, that is methods or classes that know and do too much thereby violating the single responsibility principle. The list goes on and on.

A common code smell in the source code is when a developer creates two or more methods that perform the same action with little or no variation or with program details or facts that ought to be implemented in a single point replicated in several methods or classes, leading to a code base that is not easy to maintain.

The following two ASP.NET MVC action methods have lines of code that create a strongly-typed list of strings of years and months. These lines of code, that could easily have been refactored into a third method and called by both methods, have been replicated in these two methods:

[HttpGet]
public ActionResult GetAllTransactions()
{
    List<string> years = new List<string>();
    for (int i = DateTime.Now.Year; i >= 2015; i--)
         years.Add(i.ToString());
    List<string> months = new List<string>();
    for (int j = 1; j <= 12; j++)
         months.Add(j.ToString());
    ViewBag.Transactions= GetTransactions(years,months);
     return View();
}


[HttpGet]
public ActionResult SearchTransactions()
{
    List<string> years = new List<string>();
    for (int i = DateTime.Now.Year; i >= 2015; i--)
         years.Add(i.ToString());
    List<string> months = new List<string>();
    for (int j = 1; j <= 12; j++)
        months.Add(j.ToString());
    ViewBag.Years = years;
    ViewBag.Months = months;
    return View();
}

Another common code smell occurs when developers create methods with a long list of parameters, as in the following method:

public void ProcessTransaction(string  username, string password, float transactionAmount, string transactionType, DateTime time, bool canProcess, bool retryOnfailure)
{
    //Do something
}

Bad or broken designs

Quite often, the structure or design and patterns used in implementing an application can result in bad code, most especially when object-oriented programming principles or design patterns are wrongly used. A common anti-pattern is spaghetti coding. It is common among developers with little grasp of object-orientation and this involves creating a code base with unclear structures, little or no reusability, and no relationships between objects and components. This leads to applications that are difficult to maintain and extend.

There is a common practice among inexperienced developers, which is the unnecessary or inappropriate use of design patterns in solving application complexity. The design patterns when used incorrectly can give a code base bad structure and design. The use of design patterns should simplify complexity and create readable and maintainable solutions to software problems. When a pattern is causing a readability issue and overtly adding complexity to a program, it is worth reconsidering whether to use the pattern at all, as the pattern is being misused.

For example, a singleton pattern is used to create a single instance to a resource. The design of a singleton class should have a private constructor with no arguments, a static variable with reference to the single instance of the resource, and a managed public means of referencing the static variable. A singleton pattern can simplify the access to a single-shared resource but can also cause a lot of problems when not implemented with thread safety in mind. Two or more threads can access the if (smtpGateway==null) line at the same time, which can create multiple instances of the resource if the line is evaluated to true, as with the implementation shown in the following code:

public class SMTPGateway
{
    private static SMTPGateway smtpGateway=null;

    private SMTPGateway()
    {
    }

    public static SMTPGateway SMTPGatewayObject
    {
        get
        {
            if (smtpGateway==null)
            {
                smtpGateway = new SMTPGateway();
            }
            return smtpGateway;
        }
    }
} 

Naming the program elements

Meaningful and descriptive element naming can greatly improve the source code's readability. It allows easy comprehension of the logical flow of the program. It is amazing how software developers still give names to program elements that are too short or not descriptive enough, such as giving a variable a letter name or using acronyms for variable naming. 

Generic or elusive names for elements lead to ambiguity. For example, having a method name as Extract() or Calculate() at first glance results in subjective interpretations. The same is applicable to using vague names for variables. For example:

int x2;

string xxya;

While program element naming in itself is an art, names are to be selected to define the purposes as well as succinctly describe the elements and ensure that the chosen names comply with the standards and rules of the programming language being used.

Note

More information on acceptable naming guidelines and conventions is available at: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines.

Source code readability

A good code base can be easily distinguished from a bad one by how quickly a new team member or even the programmer can easily understand it after leaving it for a few years. Quite often, because of tight schedules and approaching deadlines, software development teams tend to compromise and sacrifice professionalism to meet deadlines, by not following the recommended best practices and standards. This often leads them to produce code that is not readable.

The following code snippet will perform what it is intended to do, although it contains elements written using terrible naming conventions and this affects the code's readability:

public void updatetableloginentries()
{
   com.Connection = conn;
   SqlParameter par1 = new SqlParameter();
   par1.ParameterName = "@username";
   par1.Value = main.username;
   com.Parameters.Add(par1);
   SqlParameter par2 = new SqlParameter();
   par2.ParameterName = "@date";
   par2.Value = main.date;
   com.Parameters.Add(par2);
   SqlParameter par3 = new SqlParameter();
   par3.ParameterName = "@logintime";
   par3.Value = main.logintime;
   com.Parameters.Add(par3);
   SqlParameter par4 = new SqlParameter();
   par4.ParameterName = "@logouttime";
   par4.Value = DateTime.Now.ToShortTimeString(); ;
   com.Parameters.Add(par4);
   com.CommandType = CommandType.Text;
   com.CommandText = "update loginentries set [email protected] where [email protected] and [email protected] and [email protected]";
   openconn();
   com.ExecuteNonQuery();
   closeconn();
}

Poor source code documentation

Code can be easily understood when written using the programming language's coding style and convention while avoiding the bad code pitfalls discussed earlier. However, source code documentation is very valuable and its importance in software projects cannot be overemphasized. Brief and meaningful documentation of classes and methods can give developers a quick insight into their internal structures and operations.

Understanding a complex or poorly written class becomes a nightmare when there is no proper documentation in place. When the original programmer that wrote the code is no longer around to provide clarifications, valuable productive time can be lost trying to understand how the class or method is implemented.

Non-tested code

Though many articles have been written and discussions have been initiated at various developers' conferences on different types of testing—test-driven development, behavior-driven development, and acceptance test-driven development—it is very concerning that there are developers that continuously develop and ship software applications that are not thoroughly tested or tested at all.

Shipping applications that are poorly tested can have catastrophic consequences and maintenance problems. Notable is NASA's Mars Climate Orbiter launched on December 11, 1998 that failed just as the orbiter approached Mars, due to a software error caused by an error in conversion where the orbiter's program code was calculating a metric in pounds instead of newtons. A simple unit testing of the particular module responsible for calculating the metrics could have detected the error and maybe prevented the failure.

Also, according to the State of Test-First Methodologies 2016 Report, a survey of the adoption of test-first methodologies of more than 200 software organizations from 15 different countries, conducted by a testing services company named QASymphony, revealed that nearly half of the survey respondents had not implemented a test-first methodology in the applications they had developed.

 

What we can do to prevent bad code


Writing clean code requires a conscious effort of maintaining professionalism and following best industry standards throughout the stages of the software development process. Bad code should be avoided right from the onset of software project development, because the accumulation of bad debt through bad code can slow down software project completion and create future issues after the software has been deployed to production.

To avoid bad code, you have to be lazy, as the general saying goes that lazy programmers are the best and smartest programmers because they hate repetitive tasks, such as having to go back to fix issues that could have been prevented. Try to use programming styles and approaches that avoid writing bad code, to avoid having to rewrite your code in order to fix avoidable issues, bugs, or to pay technical debts.

Loose coupling

Loose coupling is the direct opposite of tight coupling. This is a good object-oriented programming practice of separation of concerns by allowing components to have little or no information of the internal workings and implementation of other components. Communication is done through interfaces. This approach allows for an easy substitution of components without many changes to the entire code base. The sample code in the Tight couplingsection can be refactored to allow loose coupling:

//The dependency injection would be done using Ninject
public ISmppManager smppManager { get; private set; }

public void SendSMS()
{    
    smppManager.SendMessage("0802312345","Hello", "John");
}

public class SmppManager
{
    private string sourceAddress;
    private SmppClient smppClient;

    public SmppManager()
    {
       smppClient = new SmppClient();
       smppClient.Start();            
    }        

    public void SendMessage(string recipient, string message, string senderName)
    {
       // send message using referenced library            
    }    
}
public interface ISmppManager
{
    void SendMessage(string recipient, string message, string senderName);
}

Sound architecture and design

Bad code can be avoided through the use of a good development architecture and design strategy. This will ensure that development teams and organizations have a high-level architecture, strategy, practices, guidelines, and governance plans that team members must follow to prevent cutting corners and avoiding bad code throughout the development process.

Through continuous learning and improvement, software development team members can develop thick skins towards writing bad code. The sample code snippet in the Bad or broken designssection can be refactored to be thread-safe and avoid thread-related issues, as shown in the following code:

public class SMTPGateway
{
    private static SMTPGateway smtpGateway=null;
    private static object lockObject= new object();

    private SMTPGateway()
    {
    }

    public static SMTPGateway SMTPGatewayObject
    {
        get
        {
            lock (lockObject)
            {
                if (smtpGateway==null)
                {
                    smtpGateway = new SMTPGateway();
                }
            }
            return smtpGateway;
        }
    }
} 

Preventing and detecting code smell

Programming styles and coding formats that result in code smell should be avoided. By adequately paying attention to the details, bad code pointers discussed in the Code smellsection should be avoided. The replicated lines of code in the two methods of the source code mentioned in the Code smellsection can be refactored to a third method. This avoids replication of code and allows for easy modifications:

[HttpGet]
public ActionResult GetAllTransactions()
{
    var yearsAndMonths=GetYearsAndMonths();
    ViewBag.Transactions= GetTransactions(yearsAndMonths.Item1,yearsAndMonths.Item2);
    return View();
}

[HttpGet]
public ActionResult SearchTransactions()
{
    var yearsAndMonths=GetYearsAndMonths();
    ViewBag.Years = yearsAndMonths.Item1;
    ViewBag.Months = yearsAndMonths.Item2;
    return View();
}

private (List<string>, List<string>) GetYearsAndMonths(){
    List<string> years = new List<string>();
    for (int i = DateTime.Now.Year; i >= 2015; i--)
         years.Add(i.ToString());
    List<string> months = new List<string>();
    for (int j = 1; j <= 12; j++)
        months.Add(j.ToString());
    return (years,months);
}

Also, the method with a long list of parameters in the Code smell section can be refactored to use C# Plain Old CLR Object (POCO) for clarity and reusability:

public void ProcessTransaction(Transaction transaction)
{
    //Do something
}

public class Transaction
{
    public string  Username{get;set;}
    public string Password{get;set;}
    public float TransactionAmount{get;set;}
    public string TransactionType{get;set;}
    public DateTime Time{get;set;}
    public bool CanProcess{get;set;}
    public bool RetryOnfailure{get;set;}    
}

Development teams should have guidelines, principles, and coding conventions and standards developed jointly by the team members and should be constantly updated and refined. These, when used effectively, will prevent code smell in the software code base and allow for the easy identification of potential bad code by team members.

C# coding conventions

Using the guidelines in C# coding conventions facilitates the mastery of writing clean, readable, easy to modify, and maintainable code. Use variable names that are descriptive and represent what they are used for, as shown in the following code:

int accountNumber;

string firstName;

Also, having more than one statement or declaration on a line clogs readability. Comments should be on a new line and not at the end of the code. You can read more about C# coding conventions at: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions.

Succinct and proper documentation

You should always try to write self-documenting code. This can be achieved through good programming style. Write code in such a manner that your classes, methods, and other objects are self-documenting. A new developer should be able to pick your code and not have to be stressed out before understanding what the code does and its internal structure.

Coding elements should be descriptive and meaningful to provide an insight to the reader. In situations where you have to document a method or class to provide further clarity, adopt the Keep It Simple Short (KISS) approach, briefly stating the reasons for a certain decision. Check the following code snippet; nobody wants to have to read two pages of documentation for a class containing 200 lines of code:

///
/// This class uses SHA1 algorithm for encryption with randomly generated salt for uniqueness
///
public class AESEncryptor
{
    //Code goes here
}

Note

KISS also known as Keep it Simple, Stupid, is a design principle that states that most systems work at their best when they are kept simple rather than making them unnecessarily complex. The principle aims at aiding programmers to keep the code simple as much as possible, to ensure that code can be easily maintained in the future.

Why test-driven development?

Each time I enter a discussion with folks not practicing test-driven development, they mostly have one thing in common, which is that it consumes time and resources and it does not really give a return on investment. I usually reply to them by asking which is better, detecting bugs and potential bottlenecks and fixing them while the application is being developed or hotfixing bugs when the application is in production? Test-driven development will save you a lot of problems and ensure you produce robust and issue-free applications.

Building for longevity

To avoid future problems resulting from issues when making modifications to a system in production as a result of changes in user requirements, as well as bugs which get exposed because of inherent bad code in a code base and accumulated technical debt, you need to have the mindset of developing with the future in mind and embracing changes.

Use flexible patterns and always employ good object-oriented development and design principles when writing code. The requirements of most software projects change over their life cycles. It is wrong to assume that a component or part might not change, so try and put a mechanism in place to allow the application to be graceful and accept future changes.

 

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.

Note

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.

 

The TDD cycle


The TDD technique follows a tenet known as the red-green-refactor cycle, with the red state being the initial state, indicating the commencement of a TDD cycle. At the red state, the test has just been written and will fail when it is run.

The next state is the green state and it shows that the test has passed after the actual application code has been written. Code refactoring is essential to ensure code completeness and robustness. Refactoring will be repeatedly done until the code meets performance and requirement expectations:

At the beginning of the cycle, the production code to run the test against has not been written, so it is expected that the test will fail. For example, in the following code snippet, the IsServerOnline method has not been implemented yet, and when the Test_IsServerOnline_ShouldReturnTrue unit test method is run, it should fail:

public bool IsServerOnline()
{
    return false;
}

 [Fact]
 public void Test_IsServerOnline_ShouldReturnTrue() 
 { 
    bool isOnline=IsServerOnline();   

    Assert.True(isOnline);
 }

For the test to pass, you have to implement the production code iteratively. When the following IsServerOnline method is implemented, the Test_IsServerOnline_ShouldReturnTrue test method is expected to pass:

public bool IsServerOnline()
{
    string address="localhost";
    int port=8034;
    SmppManager smppManager= new SmppManager(address, port); 
    bool isOnline=smppManager.TestConnection();
    return isOnline;
}


 [Fact]
 public void Test_IsServerOnline_ShouldReturnTrue() 
 { 
    bool isOnline=IsServerOnline();   

    Assert.True(isOnline);
 }

When the test is run and it passes, showing a green color depending on the test runner you are using, this provides an immediate feedback to you on the status of the code. This gives you confidence and inner joy that the code works correctly and behaves as it is intended to.

Refactoring is an iterative endeavor, where you continuously modify the code you have earlier written to pass the test until it has attained the state of production-ready code and that it fully implements the requirements and will work for all possible use cases and scenarios. 

 

Summary


Most potential software project maintenance bottlenecks can be avoided through the use of the principles and coding patterns discussed in this chapter. Attaining professionalism requires consistency to be disciplined and holds true to good coding habits, practices, and having a professional attitude towards TDD.

Writing clean code that is easy to maintain pays off in the long term as less effort will be required to make user-requested changes and users will be kept happy when the application is always available for use with few or no bugs.

In the next chapter, we will explore the .NET Core framework, and its capabilities and limitations. Also, we will take a tour of Microsoft Visual Studio Code before reviewing the new features available in Version 7 of the C# programming language.

About the Author

  • Ayobami Adewole

    Ayobami Adewole comes from Ibadan city in Nigeria. He is very passionate about computers and what they can be programmed to do. He is an ardent lover of the .NET stack of technologies and has developed several cutting-edge enterprise applications using the platform.

    He offers consultancy services on VoIP and Unified Communication technologies, Customer Relationship Management Systems, Business Process Automation, Enterprise Application Development, and Quality Assurance.

    Browse publications by this author

Latest Reviews

(2 reviews total)
A great book for beginners, who want to be up to date and who want use Visual Studio Code with very good explanations.
Good book, an element of programming that is often neglected.

Recommended For You

C# 8.0 and .NET Core 3.0 – Modern Cross-Platform Development - Fourth Edition

Learn the fundamentals, practical applications, and latest features of C# 8.0 and .NET Core 3.0 from expert teacher Mark J. Price.

By Mark J. Price
Hands-On Design Patterns with C# and .NET Core

Apply design patterns to solve problems in software architecture and programming using C# 7.x and .NET Core 2

By Gaurav Aroraa and 1 more
Hands-On Domain-Driven Design with .NET Core

Solve complex business problems by understanding users better, finding the right problem to solve, and building lean event-driven systems to give your customers what they really want

By Alexey Zimarev
Hands-On Software Architecture with C# 8 and .NET Core 3

Design scalable and high-performance enterprise applications using the latest features of C# 8 and .NET Core 3

By Francesco Abbruzzese and 1 more