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.
To review, here's our understanding of the "Red, Green, Refactor" TDD workflow.
- Identify the simplest possible behavior the program must exhibit.
- Write a coded test for this behavior.
- Before coding, confirm the test fails.
- Implement the behavior with the least amount of code possible.
- Run the automated test to confirm it passes. If it doesn't, revisit step 4.
- Confirm all previous tests still pass. If it doesn't, revisit step 4.
- Check if code can be refactored. If so, refactor and repeat step 6.
- Repeat this process with the next 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.
We currently have a test method declaration but it's still empty:
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:
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
.
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.
Next, we want to get our test to pass with the least amount of code possible.
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.
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!
We don't have any other tests yet so we can advance to the next step.
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.
Now it's time to start the process again with the next 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.
Let’s add a second test method to our existing test file:
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));
}
}
}
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.
Because our test is already passing, the least amount of code possible is none. We can move on to the next step.
We've can confirm the test passes and move on to the next step.
Both of our tests are passing so we move on.
Our logic is still only one line. We can't refactor it further.
Let's start from the beginning again.
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.
Let's add a new coded test for this behavior:
...
[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.
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.
We'll add the least amount of code we need to make our test pass:
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.
When we run $ dotnet test
again, our latest test passes.
We can confirm all 3 of our tests are passing. Our new logic didn't accidentally break any of our old logic.
Furthermore, we still can't refactor our code further. It's already pretty efficient!
Now we repeat the entire process for the next behavior!
There is one last scenario: Any year divisible by 400 is also leap year. Let's tackle this functionality next.
Let's add one more test to check for the appropriate behavior:
...
[TestMethod]
public void IsLeapYear_MultiplesOfFourHundred_True()
{
LeapYear testLeapYear = new LeapYear();
Assert.AreEqual(true, testLeapYear.IsLeapYear(2000));
}
...
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
Let's update our code to make our last test pass:
...
public bool IsLeapYear(int year)
{
if (year % 400 == 0)
{
return true;
}
else if (year % 100 == 0)
{
return false;
}
else
{
return year % 4 == 0;
}
}
...
After running tests again, our latest test passes.
We can confirm all 4 tests are passing. This new logic hasn't broken any previous functionality.
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