Info |
---|
You are going to follow a TDD approach to find the highest score for a series of topics |
Given the following specification
...
An organisation delivers several topics (subjects). Students are graded against each topic. You are required to store the top score for each topic.
We’ve designed the application so that it comprises of three core classes:
A class to find the highest number from an array of integers.
A class to find the highest score for a topic.
A class to write the topic and score to a file on the disk.
...
Info |
---|
You are going to follow a TDD approach to find the highest score for a series of topics |
Given the following specification
If the input is [{“Physics”, {56, 67, 45, 89}}], the result should be [{“Physics”, 89}]
If the input is [] the result should be []
If the input is [{“Physics”, {56, 67, 45, 89}}, {“Art”, {87, 66, 78}], the result should be [{“Physics”, 89}, {“Art”, 87}]
If the input is [{“Physics”, {56, 67, 45, 89}}, {“Art”, {87, 66, 78}}, {“Comp Sci”, {45, 88, 97, 56}}], the result should be [{“Physics”, 89}, {“Art”, 87}, {“Comp Sci”, 97}]
...
Create a new Test Project in the FindHighestNumber Solution
Install the packages you want (MSTest already installed, or NUnit - we are going to be using NUnit)
Create a new setup the new test class as shown here (remember you will have errors)
Code Block language c# namespace TopicManagerTests { public class TopicManagerTests { [Test] public void find_heighest_score_in_empty_array_return_empty_array() { // Arrange int[] array = {}; TopicManager cut = new TopicManager(); int[] expectedResult = {}; // Act Topic[] result = cut.findTopicHighScores(array); // Assert Assert.That(result, Is.EqualTo(result)); } } }
Create a new class called TopicManager in the FindHighestNumberService project
Clean the code up so that the class is in a namespace called TopicManagerService
Lines 9, 11, and 14 give us enough information to allow us to start thinking about what we are trying to design here. based on the requirements, we want to pass into the method
findTopicHighScores
an array of Topics and their accompanying scores. It should return the topic score of each Topic, we will call this TopicTopScore. Each item in the array being passed into thefindTopicHighScores
will be calledTopicScores
(plural I know, but it matches the context, you may want to lose the ‘s' at the end of the class name). Let’s refactor the code to reflect what we have outlined here.Code Block language c# public class TopicManagerTests { [Test] public void find_heighest_score_in_empty_array_return_empty_array() { // Arrange TopicScores[] array = Array.Empty<TopicScores>(); TopicManager cut = new TopicManager(); TopicTopScore[] expectedResult = Array.Empty<TopicTopScore>(); // Act TopicTopScore[] result = cut.findTopicHighScores(array); // Assert Assert.That(result, Is.EqualTo(result)); } }
We are now in a good position to get VS to generate the
findTopicHighScores
with the correct signatureCode Block language c# namespace TopicManagerService { public class TopicManager // TopicManager.cs { public TopicTopScore[] findTopicHighScores(TopicScores[] array) { // This block oc code assumes that the input is an empty array, there is no need for an if statement return Array.Empty<TopicTopScore>(); } } }
Now we need to complete the first piece of implementation code to pass the test
So you should have will also need the following pieces of code
Code Block namespace TopicManagerService { public class TopicTopScore // TopicTopSscore.cs { } }
Code Block namespace TopicManagerService { public class TopicScores // TopicScores.cs { } }
...
The HighestNumberFinder
is designed to return an integer representing the number in a group of numbers. This can easily be stubbed out.Here is the Stub
Begin by creating a new test
Code Block | ||
---|---|---|
| ||
namespace TopicManagerService {[Test] public class HighestNumberFindervoid find_heighest_score_with_array_of_one_return_array_of_one_using_stub() { { public int findHighestNumber(int[] values) // Arrange { int[] scores = return 89{ 56, 67, 45, 89 }; } } } |
But when you try and use this class in the tests, you should see type errors. This is because, in the implementation unit of TopicManager
, it's typed against HighestNumberFinder
in a namespace called FindHighestNumberService.v6
. So namespaces do not offer us the solution we are looking for.
Here are the modified tests
Code Block | ||
---|---|---|
| ||
using NUnit.Framework; namespace TopicManagerService { public class TopicManagerTests { string topicName = "Physics"; TopicScores[] topicScores = { new TopicScores(topicName, scores) }; IHighestNumberFinder hnf = new TopicManagerServiceStubs.HighestNumberFinder(); TopicManager [Test]cut = new TopicManager(hnf); public void find_heighest_score_in_empty_array_return_empty_array() { TopicTopScore[] expectedResult = { new TopicTopScore(topicName, 89) }; // ArrangeAct TopicScoresTopicTopScore[] topicScoresresult = Arraycut.Empty<TopicScores>findTopicHighScores(topicScores); HighestNumberFinder hnf// =Assert new HighestNumberFinder(); TopicManager cut = new TopicManager(hnfAssert.AreEqual(result[0].TopicName, expectedResult[0].TopicName); TopicTopScoreAssert.AreEqual(result[0].TopScore, expectedResult = Array.Empty<TopicTopScore>([0].TopScore); } // Act } |
Here is the Stub. We have put it in a different namespace in the test project
Code Block | ||
---|---|---|
| ||
namespace TopicManagerServiceStubs { public class HighestNumberFinder TopicTopScore[]{ result = cut.findTopicHighScores(topicScores); public int findHighestNumber(int[] values) // Assert { Assert.That(result, Is.EqualTo(expectedResult)) return 89; } [Test] public void find_heighest_score_with_array_of_one_return_array_of_one() { // Arrange int[] scores = { 56, 67, 45, 89 }; string topicName = "Physics"; TopicScores[] topicScores = { new TopicScores(topicName, scores) }; } } |
But when you try and use this class in the tests, you should see type errors in all the tests. This is because, in the implementation unit of TopicManager
, it's typed against HighestNumberFinder
in a namespace called FindHighestNumberService.v6
. So namespaces do not offer us the solution we are looking for.
Here are the updated tests. Alternatively, you could modify TopicManager
so it has a default constructor, but you will need to think about different issues and more tests for if TopicManager
default constructor is used.
Code Block | ||
---|---|---|
| ||
using NUnit.Framework; namespace TopicManagerService { public class TopicManagerTests { [Test] HighestNumberFinder hnf =public new HighestNumberFinder();void find_heighest_score_in_empty_array_return_empty_array() { TopicManager cut = new TopicManager(hnf);// Arrange TopicTopScoreTopicScores[] expectedResulttopicScores = {new TopicTopScore(topicName, 89)}Array.Empty<TopicScores>(); HighestNumberFinder //hnf Act= new HighestNumberFinder(); TopicTopScore[] result = cut.findTopicHighScores(topicScoresTopicManager cut = new TopicManager(hnf); TopicTopScore[] expectedResult = Array.Empty<TopicTopScore>(); // AssertAct Assert.AreEqual(resultTopicTopScore[0].TopicName, expectedResult[0].TopicName] result = cut.findTopicHighScores(topicScores); Assert.AreEqual(result[0].TopScore, expectedResult[0].TopScore // Assert Assert.That(result, Is.EqualTo(expectedResult)); } } } |
Observe errors on lines 14 and 32.
We need to use interfaces. And the interface needs to be in a place that is available to both production code and tests.
...
Create a new interface that is part of the HighestNumberFinder
project
...
language | c# |
---|
...
[Test] public void find_heighest_score_with_array_of_one_return_array_of_one() |
...
|
...
{ |
...
|
...
// Arrange |
...
|
...
|
...
|
...
|
...
Copy HighestNumberFinder.v6 to HighestNumberFinderFinal
and refactor the class so that it implements the interface above
...
language | c# |
---|
...
|
...
|
...
|
...
int[] |
...
scores |
...
= { 56, 67, 45, 89 |
...
}; |
...
|
...
|
...
|
...
|
...
string topicName = |
...
"Physics"; |
...
|
...
|
...
|
...
TopicScores[] topicScores = { new TopicScores(topicName, scores) }; |
...
|
...
|
...
|
...
HighestNumberFinder |
...
hnf |
...
= new HighestNumberFinder(); |
...
|
...
|
...
TopicManager cut = new TopicManager(hnf); |
...
TopicTopScore[] expectedResult = |
...
{new TopicTopScore(topicName, 89)}; // Act |
...
|
...
|
...
|
...
|
...
|
...
TopicTopScore[] result = |
...
cut.findTopicHighScores(topicScores); |
...
// |
...
Assert |
...
|
...
|
...
|
...
|
...
|
...
|
...
Assert.AreEqual(result[0].TopicName, expectedResult[0].TopicName); |
...
|
...
Assert.AreEqual(result[0].TopScore, expectedResult[0].TopScore); } |
...
}
} |
To give us more flexibility, we need to use an interface. And the interface needs to be in a place that is available to both production code and tests.
Create a new interface that is part of the
HighestNumberFinder
production projectCode Block language c# namespace FindHighestNumberService { public interface IHighestNumberFinder { } int findHighestNumber(int[] values); return result; } } }
Refactor the tests so that the
HighestNumberFinder
is instantiated as followsCode Block IHighestNumberFinder hnf = new FindHighestNumberServiceFinal.HighestNumberFinder();
Refactor
TopicManager
so it now works with the interfaceIHighestNumberFinder
and not directly with the implementation classRerun all your tests they should still be passing.
Tip |
---|
The tests have given us the confidence to refactor the code and begin to think more clearly about our implementation. |
Now let’s substitute a stub in place of the real implementation of HighestNumberFinder
.
Create a new called
find_heighest_score_with_array_of_one_return_array_of_one_using_stub()
, it's a copy offind_heighest_score_with_array_of_one_return_array_of_one()
Code Block language c# } }
Copy HighestNumberFinder.v6 to
HighestNumberFinderFinal
Refactor the class so that it implements the interface above
Code Block language c# using System; using FindHighestNumberService; namespace FindHighestNumberServiceFinal { public class EmptyArrayException : Exception { public EmptyArrayException(string message):base(message) { } } public class HighestNumberFinder : IHighestNumberFinder { public int findHighestNumber(int[] values) { [Test] if (values.Length < 1) public void find_heighest_score_with_array_of_one_return_array_of_one_using_stub() throw {new EmptyArrayException("Array is empty"); // Arrange int result = Int32.MinValue; int[] scores = { 56, 67, 45, 89for };(int i = 0; i < values.Length; i++) string topicName = "Physics"; { TopicScores[] topicScores = { new TopicScores(topicName, scores) }; if (values[i] > result) IHighestNumberFinder hnf = new FindHighestNumberServiceFinal.HighestNumberFinder(); result = values[i]; TopicManager cut = new TopicManager(hnf); } TopicTopScore[] expectedResult = { new TopicTopScore(topicName, 89) } return result; } // Act } }
Modify the Stub version so it also implements
IHighestNumberFinder
Refactor
TopicManager
so it now works with the interfaceIHighestNumberFinder
and not directly with the implementation classCode Block language c# namespace TopicManagerServiceStubs { public class TopicManager TopicTopScore[] result{ = cut.findTopicHighScores(topicScores); private IHighestNumberFinder highestNumberFinder; // Assert public TopicManager( IHighestNumberFinder Assert.AreEqual(result[0].TopicName, expectedResult[0].TopicName);hnf ) { Assert.AreEqual(result[0].TopScore, expectedResult[0].TopScore); highestNumberFinder = hnf; } }
Modify line 8 to
The error is because of TopicManagerTestsCode Block language c# IHighestNumberFinder hnf = new TopicManagerService.HighestNumberFinder();
HighestNumberFinder does not implement the.
IHighestNumberFinder
interface. Modify it so that it does.Rerun the tests, they should all pass.
Rerun all your tests they should still be passing.
Tip |
---|
The tests have given us the confidence to refactor the code and begin to think more clearly about our implementation. |
Info |
---|
We’ve now created a controlled environment for our tests |
QLC-2.3 Limitations of Stubs, complete
...
this TopicManager
...
requirement
Add more tests to handle the last two requirements
•If the input is [{“Physics”, {56, 67, 45, 89}}, {“Art”, {87, 66, 78}], the result should be [{“Physics”, 89}, {“Art”, 87}]
Only one of the tests passes. Why?
QLC-2.4 Mocks, complete the last TopicManager requirement
•If the input is [{“Physics”, {56, 67, 45, 89}}, {“Art”, {87, 66, 78}}, {“Comp Sci”, {45, 88, 97, 56}}], the result should be [{“Physics”, 89}, {“Art”, 87}, {“Comp Sci”, 97}]