Test negative cases first

What does it mean to test negative cases first? In many computer games, especially role-playing games, it is common for the game designers to make it very difficult to win the game if you simply go straight to the boss. Instead, you must make side quests, make wrong turns, and get lost in the story before you can fight the boss. Testing is no different. Before the problem can be solved, we must first handle bad input, prevent exceptions, and resolve conflicts in the business requirements.

In the Todo application, we mistakenly flew through and added an item to the Todo list without verifying that the item was valid. Now, the sprint is over and our user interface developers are mad at us because they do not know what to do with a Todo item that has no details at all. What we should have done is handle the cases where we receive bad data first. Let's rewind and temporarily skip the test we just made. 

[Fact(Skip="Forgot to test negative cases first")]
public void ItAddsATodoItemToTheTodoList()

The test we need to write now should go above the test that was just ignored, but in the same file. Remembering that we need to have small test increments, we can write a test that guards against the simplest bad data, null.

[Fact]
public void OnNullAnArgumentNullErrorOccurs()
{
// Arrange
var todo = new TodoList();
Todo item = null;

// Act
var exception = Record.Exception(() => todo.AddTodo(item));

// Assert
Assert.NotNull(exception);
Assert.IsType<ArgumentNullException>(exception);
}

public void AddTodo(Todo item)
{
throw new ArgumentNullException();
}

Notice that we have removed the code that was in place for AddTodo. We could have left the code in place, but at this point it is clutter and there is currently no test that forces that code to be present. Sometimes, when you ignore a test, it is easier to remove the functionality that test was verifying instead of working around the functionality. There are times when the clutter could restrict your refactoring efforts and could result in worse code. Don't be afraid to delete code for tests that are being skipped, and don't be afraid to delete skipped tests that make their way into source control.

One other issue that we encountered when making this change is that the AddTodoExists method defined earlier in the TodoApplicationTests class is now failing. This test was a yak shaving test to start with and does not add any real value to the test suite, so just remove it.

Now that we have the null case covered by our method, what is the next thing that could go wrong? Thinking about it, are there any required fields for a Todo? We should probably make sure the Todo has a title or description at least before we add it to the list.

First, before we can verify that the field has been populated, we need to verify that the field exists on the model. Writing model tests might seem a bit like overkill to you, but we find that having these tests helps to better define the application for others coming into it. They also provide a good attachment point for field validation tests later on when your business decides that the description field of a Todo has a maximum length of 255 characters. Let's create a new class for the Todo model tests.

public class TodoModelTests
{
[Fact]
public void ItHasDescription()
{
// Arrange
var todo = new Todo();

// Act
todo.Description = "Test Description";
}
}

public class Todo
{
public string Description { get; set; }
}

As you can see, there is no real assert for this type of test. Simply verifying that we can set the description value without throwing an error will suffice.

Now that we have a description field, we can verify that it is required.

[Fact]
public void OnNullADescriptionRequiredValidationErrorOccurs()
{
// Arrange
var todo = new TodoList();
var item = new Todo()
{
Description = null
};

// Act
var exception = Record.Exception(() => todo.AddTodo(item));

// Assert
Assert.NotNull(exception);
Assert.IsType(typeof(DescriptionRequiredException), exception);
}

internal class TodoList
{
...

public void AddTodo(Todo item)
{
item = item ?? throw new ArgumentNullException();

item.Description = item.Description ?? throw new
DescriptionRequiredException();
}
}

We are long overdue for some refactoring and this is a good place to pause our testing efforts and refactor. We would like to move the model validation into the model. Let's create a quick test for a validation method on the Todo model and then move that logic into the Todo class.

public class TodoModelValidateTests
{
[Fact]
public void ItExists()
{
// Arrange
var todo = new Todo();

// Act
todo.Validate();
}
}

public class Todo
{
public string Description { get; set; }

internal void Validate()
{
}
}

Now, at least for the moment, we want to move our validation logic over from the Todo list into the model. In creating the validation test and moving the logic, we have caused our yak shaving test to fail. The test is failing because, although the required method exists, it is throwing an exception because we have not populated the description of our Todo. We will have to remove this test as it no longer adds value.

public class TodoModelValidateTests
{
[Fact]
public void OnNullADescriptionRequiredValidationErrorOccurs()
{
// Arrange
var item = new Todo()
{
Description = null
};

// Act
var exception = Record.Exception(() => item.Validate());

// Assert
Assert.NotNull(exception);
Assert.IsType(typeof(DescriptionRequiredException), exception);
}
}

public class Todo
{
public string Description { get; set; }

internal void Validate()
{
Description = Description ?? throw new DescriptionRequiredException();
}
}

Finally, the tests we needed to write before we could make the refactoring change we wanted to make are complete. Now we can simply replace the exception logic dealing with model validation in the TodoList class with a call to Validate on the model.

public void AddTodo(Todo item)
{
item = item ?? throw new ArgumentNullException();

item.Validate();
}

This change should have no effect on our tests or our resulting logic. We are simply relocating the validation code. There are many more validations that could happen. Can you think of a few that might be valuable?

It is now time to add back in our skipped test, with some minor modifications to pass validation.

[Fact]
public void ItAddsATodoItemToTheTodoList()
{
// Arrange
var todo = new TodoList();
var item = new Todo
{
Description = "Test Description"
};

// Act
todo.AddTodo(item);

// Assert
Assert.Single(todo.Items);
}

public void AddTodo(Todo item)
{
item = item ?? throw new ArgumentNullException();

item.Validate();

_items.Add(item);
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.8.8