- We start by a first simple test case :
9 + 3 = 12
- We create a
CalculatorShould
- We use the 3A pattern to describe it
- We assert the expected result
- We create a
public class CalculatorShould
{
[Fact]
public void SupportAdd()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Calculate(9, 3, Calculator.Add);
// Assert
Assert.Equal(12, result);
}
}
- Now that we have 1 test case we can repeat for others
✅ 9 + 3 = 12
3 x 76 = 228
9 / 3 = 3
9 - 3 = 9
Unsupported operator should fail
- Other test cases
[Fact]
public void SupportMultiply()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Calculate(3,76, Calculator.Multiply);
// Assert
Assert.Equal(228, result);
}
[Fact]
public void SupportDivide()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Calculate(9,3, Calculator.Divide);
// Assert
Assert.Equal(3, result);
}
[Fact]
public void SupportSubtract()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Calculate(9,3, Calculator.Subtract);
// Assert
Assert.Equal(6, result);
}
- Update our test cases
✅ 9 + 3 = 12
✅ 3 x 76 = 228
✅ 9 / 3 = 3
✅ 9 - 3 = 9
Unsupported operator should fail
- Let's implement the failure test
[Fact]
public void FailWhenOperatorNotSupported()
{
var calculator = new Calculator();
var exception = Assert.Throws<ArgumentException>(() => calculator.Calculate(9, 3, "UnsupportedOperator"));
Assert.Equal("Not supported operator", exception.Message);
}
You should pay the same attention to your tests as to your production code.
- Let's remove duplication
- assertion code is duplicated
- we instantiate 1
Calculator
per test, but we can use the same (no state inside)
- In
xUnit
for this kind of test we can useParameterized tests
- We define a
[Theory]
method that takes some data as input[InlineData]
- We adapt the test method as well
public void SupportOperations(int a, int b, string @operator, int expectedResult)
- We define a
- We can use a library that simplify assertion readability as well called FluentAssertions
public class RefactoredCalculatorShould
{
private readonly Calculator _calculator = new();
[Theory]
[InlineData(9, 3, Add, 12)]
[InlineData(3, 76, Multiply, 228)]
[InlineData(9, 3, Divide, 3)]
[InlineData(9, 3, Subtract, 6)]
public void SupportOperations(int a, int b, string @operator, int expectedResult) =>
_calculator
.Calculate(a, b, @operator)
.Should()
.Be(expectedResult);
[Fact]
public void FailWhenOperatorNotSupported() =>
_calculator.Invoking(_ => _.Calculate(9, 3, "UnsupportedOperator"))
.Should()
.Throw<ArgumentException>()
.WithMessage("Not supported operator");
}
There is one Edge case not yet supported, what happens if we divide by 0?
✅ 9 + 3 = 12
✅ 3 x 76 = 228
✅ 9 / 3 = 3
✅ 9 - 3 = 9
✅ Unsupported operator should fail
Divide by 0 should fail
- Write at least one test for it
public class TimeUtility
{
public string GetTimeOfDay()
{
var time = DateTime.Now;
return time.Hour switch
{
>= 0 and < 6 => "Night",
>= 6 and < 12 => "Morning",
>= 12 and < 18 => "Afternoon",
_ => "Evening"
};
}
}
- Here is the simplest test we can write
- Which problem will you encounter?
public class TimeUtilityShould
{
[Fact]
public void BeAfternoon() =>
new TimeUtility()
.GetTimeOfDay()
.Should()
.Be("Afternoon");
}
-
This test is not repeatable because the design is coupled to
LocalTime.now()
- We need to isolate it to be able to test this unitary
- A few solutions here :
- Pass a
DateTime
as method arg - Pass a
IClock
which will provide aNow()
method that we will be able to substitute - Pass a function
clock: void => DateTime
- Pass a
-
Identify some examples
6:05AM -> Morning
1:00AM -> Night
1PM -> Afternoon
11PM -> Evening
- Adapt the
TimeUtility
to inject anIClock
collaborator- Generate your code from usage
public class TimeUtility
{
private readonly IClock _clock;
public TimeUtility(IClock clock) => _clock = clock;
public string GetTimeOfDay()
{
return _clock.Now().Hour switch
{
>= 0 and < 6 => "Night",
>= 6 and < 12 => "Morning",
>= 12 and < 18 => "Afternoon",
_ => "Evening"
};
}
}
public interface IClock
{
DateTime Now();
}
-
Now our code has no
hardcoded
dependency anymore -
We need to adapt our tests
- How to provide a
IClock
in the given state for our test cases? Test Doubles
is our solution
- How to provide a
-
To use TestDoubles we need to use
moq
- Instantiate a
IClock
mock - We implement our first test case
- Instantiate a
[Fact]
public void ReturnMorningFor6AM()
{
var clockMock = new Mock<IClock>();
clockMock.Setup(c => c.Now())
.Returns(new DateTime(2022, 12, 1, 6, 5, 0, 0));
new TimeUtility(clockMock.Object)
.GetTimeOfDay()
.Should()
.Be("Morning");
}
✅ 6:05AM -> Morning
1:00AM -> Night
1PM -> Afternoon
11PM -> Evening
- Implement others by using a
Theory
once again
public class TimeUtilityShould
{
[Theory]
[InlineData(0, "Night")]
[InlineData(4, "Night")]
[InlineData(6, "Morning")]
[InlineData(9, "Morning")]
[InlineData(12, "Afternoon")]
[InlineData(17, "Afternoon")]
[InlineData(18, "Evening")]
[InlineData(23, "Evening")]
public void GetADescriptionAtAnyTime(int hour, string expectedDescription)
{
var clockMock = new Mock<IClock>();
clockMock.Setup(c => c.Now())
.Returns(hour.ToDateTime());
new TimeUtility(clockMock.Object)
.GetTimeOfDay()
.Should()
.Be(expectedDescription);
}
[Fact]
public void ReturnMorningFor6AM()
{
var clockMock = new Mock<IClock>();
clockMock.Setup(c => c.Now())
.Returns(new DateTime(2022, 12, 1, 6, 5, 0, 0));
new TimeUtility(clockMock.Object)
.GetTimeOfDay()
.Should()
.Be("Morning");
}
}
internal static class TestExtensions
{
public static DateTime ToDateTime(this int hour)
=> new(2022, 12, 1, hour, 0, 0, 0);
}