Lesson Sunday

We have an empty sample test that is technically passing but doesn't actually have any logic in it. At this point, we're ready to start writing tests for our code.

We will continue to use test-driven development in C#. Remember that the purpose of test-driven development is to write a test for the smallest unit of behavior possible. The test should fail first (and should be a good fail). Then we should add the smallest amount of code possible to get the test to pass. After that, we can refactor our code as necessary. This follows the "Red, Green, Refactor" TDD workflow.

Keep in mind, though, that the process of using TDD with C# will feel very different. When there is an error in our C# code, our code will often fail to compile - an issue we won't run into with JavaScript. We still need to make sure we have a good fail - and code that fails to compile is not a good fail.

"Red, Green, Refactor" Review


To review, here's our understanding of the "Red, Green, Refactor" TDD workflow.

  1. Identify the simplest possible behavior the program must exhibit.
  2. Write a coded test for this behavior.
  3. Before coding, confirm the test fails.
  4. Implement the behavior with the least amount of code possible.
  5. Run the automated test to confirm it passes. If it doesn't, revisit step 4.
  6. Confirm all previous tests still pass. If it doesn't, revisit step 4.
  7. Check if code can be refactored. If so, refactor and repeat step 6.
  8. Repeat this process with the next simplest behavior.

MSTest Specs with "Red, Green, Refactor" Workflow


1. Identify the Simplest Behavior

In the last lesson, we identified the simplest possible behavior for our IsLeapYear() method. It should check to see if a number is divisible by four.

2. Write a Coded Test

We currently have a test method declaration but it's still empty:

Calendar.Tests/ModelTests/LeapYearTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Calendar;

namespace Calendar.Tests
{
  [TestClass]
  public class LeapYearTests
  {
    [TestMethod]
    public void IsLeapYear_NumberDivisibleByFour_True()
    {
      // eventually your testing code will go here
    }
  }
}

Let's add code to this test method now:

Calendar.Tests/ModelTests/LeapYearTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Calendar;

namespace Calendar.Tests
{
  [TestClass]
  public class LeapYearTests
  {
    [TestMethod]
    public void IsLeapYear_NumberDivisibleByFour_True()
    {
      LeapYear testLeapYear = new LeapYear();
      Assert.AreEqual(true, testLeapYear.IsLeapYear(2012));
    }
  }
}

We create an instance of our LeapYear class with the line LeapYear testLeapYear = new LeapYear();. Then we write our first assertion using a method from the Assert class: AreEqual(). AreEqual() checks whether the two arguments provided are equal. In our case, it will check if true and testLeapYear.isLeapYear(2012) are equal.

The first argument is what we expect the result of the test to be. The second is the expression to be evaluated. For example, Assert.AreEqual(true, 1 == 1) would be a passing test because the first argument true is equal to the second argument 1 == 1.

3. Confirm the Test Fails

Let's confirm that our test fails. We know it should because our IsLeapYear() method only returns false.

Run $ dotnet test in the Calendar.Tests directory. We'll receive a response that looks like this:

...

Starting test execution, please wait...
Failed   Calendar.Tests.LeapYearTests.IsLeapYear_NumberDivisibleByFour_True
Error Message:
 Assert.AreEqual failed. Expected:<True>. Actual:<False>.
Stack Trace:
   at Calendar.Tests.LeapYearTests.IsLeapYear_NumberDivisibleByFour_True() in /Users/epicodus_staff/Desktop/Calendar.Solution/Calendar.Tests/ModelTests/LeapYearTests.cs:line 13


Total tests: 1. Passed: 0. Failed: 1. Skipped: 0.
Test Run Failed.
Test execution time: 0.9755 Seconds

Our test successfully fails. This is the red portion of our "Red, Green, Refactor" workflow.

4. Implement the Behavior with the Least Amount of Code Possible

Next, we want to get our test to pass with the least amount of code possible.

Calendar/Models/LeapYear.cs
namespace Calendar
{
  public class LeapYear
  {
    public bool IsLeapYear(int year)
    {
      return year % 4 == 0;
    }
  }
}

We use a modulus to determine whether year is divisible by 4. If it is, our method will return true. This is the least amount of code possible to make our one single test pass. Nothing more.

5. Confirm The Test Passes

Run the $ dotnet test command from within the Calendar.Tests project:

...

Starting test execution, please wait...

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 0.8462 Second

Our test is passing. We're ready to move on to the next step!

6. Confirm Previous Tests Still Pass

We don't have any other tests yet so we can advance to the next step.

7. Check For Refactoring

Once our code is working and a test passes, we should look for opportunities to improve our code. If we accidentally break something, our tests will let us know.

In our case, our logic includes only one line and we can't refactor it further. Even so, it's always important to check if refactoring is possible before moving onto the next behavior.

8. Repeat

Now it's time to start the process again with the next simplest behavior.

1. Identify the Simplest Behavior

We just confirmed our program can successfully return true if a provided year is divisible by four. Let's also make sure it can return false if a provided year is not divisible by four.

This may seem mundane but it's truly the next simplest behavior.

2. Write a Coded Test

Let’s add a second test method to our existing test file:

Calendar.Tests/ModelTests/LeapYearTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Calendar;

namespace Calendar.Tests
{
  [TestClass]
  public class LeapYearTests
  {

    [TestMethod]
    public void IsLeapYear_NumberDivisibleByFour_True()
    {
      LeapYear testLeapYear = new LeapYear();
      Assert.AreEqual(true, testLeapYear.IsLeapYear(2012));
    }

    [TestMethod]
    public void IsLeapYear_NumberNotDivisibleByFour_False()
    {
      LeapYear testLeapYear = new LeapYear();
      Assert.AreEqual(false, testLeapYear.IsLeapYear(1999));
    }

  }
}

3. Confirm the Test Fails

Let's run $ dotnet test from Calendar.Tests:

...

Total tests: 2. Passed: 2. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 0.8380 Seconds

...

Our test already passes. This is because the functionality to return a boolean based on whether a provided year is divisible by four is actually already in place. However, we want to have a test for both True and False statements to ensure we don't break our method later when we add more code.

If a spec is testing the opposite of a piece of functionality already included, like this one here, it may pass at this stage. However, a test for brand new functionality we haven't implemented yet shouldn't pass at this stage.

4. Implement the Behavior with the Least Amount of Code Possible

Because our test is already passing, the least amount of code possible is none. We can move on to the next step.

5. Confirm The Test Passes

We've can confirm the test passes and move on to the next step.

6. Confirm Previous Tests Still Pass

Both of our tests are passing so we move on.

7. Check For Refactoring

Our logic is still only one line. We can't refactor it further.

8. Repeat

Let's start from the beginning again.

1. Identify the Simplest Behavior

What's the next simplest behavior? Years divisible by 100 are not leap years so we'll want our program to return false when a year divisible by 100 is provided.

2. Write a Coded Test

Let's add a new coded test for this behavior:

Calendar.Tests/ModelTests/LeapYearTests.cs
...

    [TestMethod]
    public void IsLeapYear_MultiplesOfOneHundred_False()
    {
      LeapYear testLeapYear = new LeapYear();
      Assert.AreEqual(false, testLeapYear.IsLeapYear(1900));
    }

...

Here, we assert that the result of testLeapYear.IsLeapYear(1900) should be false. If it is, the test will pass. If not, it will fail.

3. Confirm the Test Fails

Let's run our tests again.

Failed   Calendar.Tests.LeapYearTests.IsLeapYear_MultiplesOfOneHundred_False
Error Message:
 Assert.AreEqual failed. Expected:<False>. Actual:<True>.
Stack Trace:
   at Calendar.Tests.LeapYearTests.IsLeapYear_MultiplesOfOneHundred_False() in /Users/epicodus_staff/Desktop/Calendar.Solution/Calendar.Tests/ModelTests/LeapYearTests.cs:line 28

Total tests: 3. Passed: 2. Failed: 1. Skipped: 0.
Test Run Failed.
Test execution time: 0.8335 Seconds

Here's the error message:

Failed   Calendar.LeapYearTests.IsLeapYear_MultiplesOfOneHundred_False

Our most recent test has failed. We're ready for the next step.

4. Implement the Behavior with the Least Amount of Code Possible

We'll add the least amount of code we need to make our test pass:

Calendar/Models/LeapYear.cs
namespace Calendar
{
  public class LeapYear
  {
    public bool IsLeapYear(int year)
    {
      if (year % 100 == 0)
      {
        return false;
      }
      else
      {
        return year % 4 == 0;
      }
    }
  }
}

This will return false when a year is divisible by 100.

5. Confirm The Test Passes

When we run $ dotnet test again, our latest test passes.

6. Confirm Previous Tests Still Pass

We can confirm all 3 of our tests are passing. Our new logic didn't accidentally break any of our old logic.

7. Check For Refactoring

Furthermore, we still can't refactor our code further. It's already pretty efficient!

8. Repeat Again

Now we repeat the entire process for the next behavior!

1. Identify the Simplest Behavior

There is one last scenario: Any year divisible by 400 is also leap year. Let's tackle this functionality next.

2. Write a Coded Test

Let's add one more test to check for the appropriate behavior:

Calendar.Tests/ModelTests/LeapYearTests.cs
...
[TestMethod]
public void IsLeapYear_MultiplesOfFourHundred_True()
{
  LeapYear testLeapYear = new LeapYear();
  Assert.AreEqual(true, testLeapYear.IsLeapYear(2000));
}
...

3. Confirm the Test Fails

If we save and re-run the $ dotnet test command, we should see this latest test fail:

Failed   Calendar.Tests.LeapYearTests.IsLeapYear_MultiplesOfFourHundred_True
Error Message:
 Assert.AreEqual failed. Expected:<True>. Actual:<False>.
Stack Trace:
   at Calendar.Tests.LeapYearTests.IsLeapYear_MultiplesOfFourHundred_True() in /Users/epicodus_staff/Desktop/Calendar.Solution/Calendar.Tests/ModelTests/LeapYearTests.cs:line 35


Total tests: 4. Passed: 3. Failed: 1. Skipped: 0.
Test Run Failed.
Test execution time: 1.0405 Seconds

4. Implement the Behavior with the Least Amount of Code Possible

Let's update our code to make our last test pass:

Calendar/Models/LeapYear.cs
...
public bool IsLeapYear(int year)
{
  if (year % 400 == 0)
  {
    return true;
  }
  else if (year % 100 == 0)
  {
    return false;
  }
  else
  {
    return year % 4 == 0;
  }
}
...

5. Confirm The Test Passes

After running tests again, our latest test passes.

6. Confirm Previous Tests Still Pass

We can confirm all 4 tests are passing. This new logic hasn't broken any previous functionality.

7. Check For Refactoring

This practice program is fairly simple so our code is already concise, which means there's no need to refactor further.

In this lesson, we covered our TDD workflow in depth. Following this process helps us concentrate on making incremental, targeted changes to our code to meet specific objectives. It's also fun to get our tests passing!

Lesson 7 of 20
Last updated April 14, 2022