How to design unit test cases

Recently, some large companies are carrying out the operation of de testing. The root of all this can probably start from the fact that Microsoft cut down all the internal formal testers a few years ago. At that time, some of the internal test engineers of Microsoft were transferred to development engineers, and a large part of their functions were to teach ordinary developers how to test. We all know that the testing conducted by developers is generally based on unit testing. If one day your organization needs you to become a testing coach and promote unit testing in addition to automated testing, how do you define the design methodology of unit testing examples? Here are some ideas to see how to design simple unit test cases.

A method can have any number of valid test cases; It ultimately depends on the structure of the method. There are two simple ways to help us design unit test cases.

  • Parameter method
  • Execution path method

I will demonstrate this by providing real code. All code snippets will be written in C# and assertions will use my favorite unit test package, Fluent Assertions.

We will provide test cases for the following methods:

public static bool ContainsNamelessItems(this List<Item> items)
{
  return items.Any(item => item.Name.IsNullOrEmpty())
}

This method takes the project collection as a parameter. It iterates through the list of items and checks whether the name attribute is empty for each Item. If name exists and is not empty, we return True, otherwise we return False.

Create test cases using parametric methods

This method mainly considers what values the input parameter can pass.

Check the parameter containsnamelesitems of this method. We have a List named items This parameter may have several possible values:

  • items is empty
  • items contains at least 1 Item with an attribute whose Name is undefined
  • Items does not contain items with undefined Name attribute
  • items is null

Each of these possible values can exist as a separate use case.

Here are some possible test cases and assertions:

1. When list < item > is empty, we expect the return value to be False because its list < item > has no name attribute.

public void WhenItemsIsEmpty_ReturnFalse()
{
  var items = new List<Item>();

  var result = items.ContainsNamelessItems();

  result.Should()
    .BeFalse("because an empty collection cannot contain nameless items");
}

2. When list < Item > contains at least one Item without name attribute, we expect the return value to be True

public void WhenItemsContainsANamelessItem_ReturnTrue()
{
  var items = new List<Item>
  {
    { new Item { Name = "Item1" },
    { new Item { Name = string.Empty } // nameless item
  };

  var result = items.ContainsNamelessItems();

  result.Should()
    .BeTrue("because there is a nameless item in the collection");
}

3. When list < item > does not contain any items without name attribute, we expect the return value to be False because all items have name.

public void WhenItemsDoesNotContainANamelessItem_ReturnFalse()
{
  var items = new List<Item>
  {
    { new Item { Name = "Item1" },
    { new Item { Name = "Item2" }
  };

  var result = items.ContainsNamelessItems();

  result.Should()
    .BeFalse("because there are no nameless items in the collection");
}

4. When list < item > is null, we expect to throw an ArgumentNullException exception, which is often the most difficult to think of.

public void WhenItemsIsNull_ThrowArgumentException()
{
  List<Item> items = null;

  Action act = () => items.ContainsNamelessItems();

  act.Should()
    .Throw<ArgumentNullException>("because the collection is null");  
}

Create test cases using the execution path method

Path mode needs to traverse the tested method and find all different execution paths.

The method we defined above has only one execution path, because there is no condition driven path except directly to the end of the method. To change the path, we need to introduce some conditions through if Else, switch, and try/catch statements. In these condition blocks, the method may exit directly when a condition is met, rather than running to the last line of the method.

Let's introduce the condition below. Suppose we don't want the method to throw an ArgumentNullException exception when the input parameter is empty, but we want to throw a custom ArgumentException. Then we must add a condition to the method that checks whether the item list is empty.

The flow chart is as follows:

Now, if the project is empty, it is possible to exit early rather than go to the end of the method. The specific implementation is as follows

public static bool ContainsNamelessItems(List<Item> items)
{
  if (items == null)
    throw new ArgumentException("The collection of items should not be null.");

  return items.Any(item => item.Name.IsNullOrEmpty())
}

The corresponding test of this test case looks like this:

public void WhenItemCollectionIsNull_ThrowArgumentException()
{
  List<Item> items = null;

  Action act = () => items.ContainsNamelessItems();

  act.Should().Throw<ArgumentException>()
    .WithMessage("The collection of items should not be null.");  
}

summary

  • When entering parameters, arbitrary parameters can be constructed in the form of equivalent classes. In strongly typed languages, invalid classes will be used relatively less. After all, the compiler will check; In weakly typed languages, invalid classes are hidden, which is the focus of testing;
  • In fact, the execution path method is branch coverage, which covers all branches through impassable input parameters. For example, when the input is also a valid class, empty sets and non empty sets may go to impassable paths;
  • When the method or function is particularly complex, you can try to disassemble the method to obtain better testability;

Keywords: unit testing

Added by Hopps on Mon, 24 Jan 2022 14:56:39 +0200