In the series we will define the basic terms that every developer needs to know about testing. The purpose is to give all team members a shared understanding of the fundamental terminology of the quality assurance and all related processes. Later this will improve the communication and reviews quality. It will further increase the testing capabilities of each member. In this part, we will talk about the basic concepts and terminology in unit testing.
As part of the professional services we provide at BELLATRIX, we consult companies and help them to improve their QA process and set up an automated testing infrastructure. After the initial review process and giving improvement recommendations for some companies we need to hire new talents that can help the company to scale-up the solutions we provided. This was part of training we did for a company we consulted so that we educate all of their developers.
Introduction
It’s been floating around since the early days of the Smalltalk programming language in the 1970s. Improve code quality while gaining a deeper understanding of the functional requirements of a class or method.
Unit of Work
Unit of Work- a unit of work is the sum of actions that take place between the invocation of a public method in the system and a single noticeable end result by a test of that system. A noticeable end result can be observed without looking at the internal state of the system and only through its public APIs and behavior.
An end result is any of the following:
The invoked public method returns a value (a function that’s not void).
There’s a noticeable change to the state or behavior of the system before and after invocation that can be determined without interrogating private state. (Examples: the system can log in a previously nonexistent user, or the system’s properties change if the system is a state machine.)
There’s a callout to a third-party system over which the test has no control, and that third-party system doesn’t return any value, or any return value from that system is ignored. (Example: calling a third-party logging system that was not written by you and you don’t have the source to.)
This idea of a unit of work means, to me, that a unit can span as little as a single method and up to multiple classes and functions to achieve its purpose.
public void ClearCart()
{
var cartItems = _appDbContext
.ShoppingCartItems
.Where(cart => cart.ShoppingCartId == ShoppingCartId);
_appDbContext.ShoppingCartItems.RemoveRange(cartItems);
_appDbContext.SaveChanges();
}
Command-Query Separation
First described by Bertrand Meyer.
Command-Query Separation- a method should be a command or a query, but not both.
Command- a method that can modify the state of the object but that doesn’t return a value.
public void ClearCart()
{
var cartItems = _appDbContext
.ShoppingCartItems
.Where(cart => cart.ShoppingCartId == ShoppingCartId);
_appDbContext.ShoppingCartItems.RemoveRange(cartItems);
_appDbContext.SaveChanges();
}
Query- a method that returns a value but that does not modify the object.
public List<ShoppingCartItem> GetShoppingCartItems()
{
return ShoppingCartItems ??
(ShoppingCartItems =
_appDbContext.ShoppingCartItems
.Where(c => c.ShoppingCartId == ShoppingCartId)
.Include(s => s.Pie)
.ToList());
}
Why Is This Principle Important?
A method should be a command or a query, but not both.
There are a number of reasons, but the most primary is communication. If a method is a query, we shouldn’t have to look at its body to discover whether we can use it several times in a row without causing some side effect.
End Result- We should not look into the private state of an object.
private string _shoppingCartId;
public static ShoppingCartService GetCart(IServiceProvider services)
{
ISession session = services.GetRequiredService<IHttpContextAccessor>()?
.HttpContext.Session;
var context = services.GetService<AppDbContext>();
string cartId = session.GetString("CartId") ?? Guid.NewGuid().ToString();
session.SetString("CartId", cartId);
return new ShoppingCartService(context) { _shoppingCartId = cartId };
}
We can check whether a third-party system is called. In the example below the _logger.
[HttpGet("{id}")]
public async Task<IActionResult> GetPie(int id)
{
try
{
return Ok(pieDto);
}
catch (Exception ex)
{
_logger.LogCritical($"Exception while ... id {id}.", ex);
return StatusCode(500, "A problem happened.");
}
}
Properties of a Good Unit Test
A unit test should have the following properties:
It should be automated and repeatable.
It should be easy to implement.
It should be relevant tomorrow.
public void ReturnCorrectDate_When_OneItemPresent()
{
var shoppingCartService = new ShoppingCartService();
shoppingCartService.AddToCart(new Pie(), 100);
var item = shoppingCartService.GetShoppingCartItems().First();
Assert.AreEqual(item.Created.Minute, DateTime.Now.Minute);
}
Anyone should be able to run it at the push of a button. It should run quickly. It should be consistent in its results.
public void ReturnCorrectDate_When_OneItemPresent()
{
var shoppingCartService = new ShoppingCartService();
shoppingCartService.AddToCart(new Pie(), 100);
var item = shoppingCartService.GetShoppingCartItems().First();
Assert.AreEqual(item.Created.Minute, DateTime.Now.Minute);
}
It should be fully isolated (runs independently of other tests).
public void AddPieToCart()
{
var shoppingCartService = new ShoppingCartService();
shoppingCartService.AddToCart(new Pie(), 100);
}
public void ReturnCorrectDate_When_OneItemPresent()
{
var shoppingCartService = new ShoppingCartService();
var item = shoppingCartService.GetShoppingCartItems().First();
Assert.AreEqual(item.Created.Minute, DateTime.Now.Minute);
}
When it fails, it should be easy to detect what was expected and determine how to pinpoint the problem.
What Is an Integration Test?
Integration testing- is testing a unit of work without having full control over all of it and using one or more of its real dependencies, such as time, network, database, threads, random number generators, and so on.
I consider integration tests as any tests that aren’t fast and consistent and that use one or more real dependencies of the units under test. For example, if the test uses the real system time, the real file-system, or a real database, it has stepped into the realm of integration testing. If a test doesn’t have control of the system time, for example, and it uses the current DateTime.Now in the test code, then every time the test executes, it’s essentially a different test because it uses a different time. It’s no longer consistent.
public void ReturnCorrectDate_When_OneItemPresent()
{
var shoppingCartService = new ShoppingCartService(new AppDbContext());
shoppingCartService.AddToCart(new Pie(), 100);
var item = shoppingCartService.GetShoppingCartItems().First();
Assert.AreEqual(item.Created.Minute, DateTime.Now.Minute);
}
The above test is an integration one because we use two dependencies- AppDbContext and DateTime.Now.
Legacy Code
Regression- a regression is one or more units of work that once worked and now don’t.
Legacy Code- source code that relates to a no-longer supported or manufactured operating system or other computer technology.
Legacy Code- source code that relates to a no-longer supported or manufactured operating system or other computer technology.
Legacy Code- any older version of the application currently under maintenance.
Legacy Code- it often refers to code that’s hard to work with, hard to test, and usually hard to read.
Legacy Code- code that works. Code that has no tests!
Definition
A unit test is an automated piece of code that invokes the unit of work being tested, and then checks some assumptions about a single end result of that unit.
Control Flow
Control flow- code is any piece of code that has some logic in it.
public decimal GetShoppingCartTotal()
{
var total = _appDbContext.ShoppingCartItems.Where(c => c.ShoppingCartId == ShoppingCartId)
.Select(c => c.Pie.Price * c.Amount).Sum();
return total;
}
Control Flow- has one or more of the following: if statement, loop, switch, case, calculations, or any other type of decision making code.
public static ShoppingCartService GetCart(IServiceProvider services)
{
ISession session = services.GetRequiredService<IHttpContextAccessor>()?
.HttpContext.Session;
var context = services.GetService<AppDbContext>();
string cartId = session.GetString("CartId") ?? Guid.NewGuid().ToString();
session.SetString("CartId", cartId);
return new ShoppingCartService(context);
}
Properties are good examples of code that usually doesn’t contain any logic and so doesn’t require specific targeting by the tests.
public string ShoppingCartId { get; set; }
public List<ShoppingCartItem> ShoppingCartItems { get; set; }
Once you add any check inside a property, you’ll want to make sure that logic is being tested.
public string ShortDescription
{
get => _shortDescription;
set
{
if (value.Length > 50)
{
throw new ArgumentException("Description should be less than 50 chars.");
}
_shortDescription = value;
}
}
When to Write Tests?
The next question is when to write the tests. Many people feel that the best time to write unit tests for software is after the software has been written, but a growing number prefer writing unit tests before the production code is written. This approach is called test-first or test-driven development (TDD)
But it’s not without a price (time to learn, time to implement, and more).
Write a failing test to prove code or functionality is missing from the end product.
Make the test pass by writing production code that meets the expectations of your test.
Refactor your code. When the test passes, you’re free to move on to the next unit test or to refactor your code to make it more readable, to remove code duplication, and so on.
Refactoring- means changing a piece of code without changing its functionality.
Refactoring- the code does the same thing but it becomes easier to- maintain, read, debug, change.
Unit Testing Framework
If you’ve ever renamed a method, you’ve done refactoring. If you’ve ever split a large method into multiple smaller method calls, you’ve refactored your code. The code still does the same thing, but it becomes easier to maintain, read, debug, and change.
Doing tests and regression testing completely manually, repeating the same actions again and again like a monkey, is error prone and time consuming, and people seem to hate doing that as much as anything can be hated in software development. These problems are alleviated by tooling. Unit testing frameworks help developers write tests more quickly with a set of known APIs, execute those tests automatically, and review the results of those tests easily.
Framework supplies the developer with a class library that contains
- Base classes or interfaces to inherit
- Attributes to place in your code to note which of your methods are tests
- Assertion classes that have special assertion methods you invoke to verify your code
Unit tests are written as code, using libraries from the unit testing framework. Then the tests are run from a separate unit testing tool or inside the IDE, and the results are reviewed. In short, what you’ve been missing is a framework for writing, running, and reviewing unit tests and their results.
Unit testing frameworks- are code libraries and modules that help developers unit test their code.
- Framework provides a test runner (a console or GUI tool) that
- Identifies tests in your code
- Runs tests automatically
- Indicates status while running
- Can be automated by the command line
At the time of this writing, there are more than 150 unit testing frameworks out there— practically one for every programming language in public use. You can find a good list
at list of unit testing frameworks Wiki. Consider that .NET alone has at least 3 different active unit testing frameworks: MSTest (from Microsoft), xUnit.net, and NUnit. Among these, NUnit was once the de facto standard.
Note- Using a unit testing framework doesn’t ensure that the tests you write are readable, maintainable, or trustworthy or that they cover all the logic you’d like to test.
Collectively, these unit testing frameworks are called the xUnit frameworks because their names usually start with the first letters of the language for which they were built. You might have CppUnit for C++, JUnit for Java, NUnit for .NET, and HUnit for the Haskell programming language.
