QLC-2.1) Setup and first test
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)
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.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 signaturenamespace 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 the following pieces of code
namespace TopicManagerService { public class TopicTopScore // TopicTopSscore.cs { } }
namespace TopicManagerService { public class TopicScores // TopicScores.cs { } }
Second test
Implement this second 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) }; TopicManager cut = new TopicManager(); 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); }
And here is the implementation class
public class TopicManager { private HighestNumberFinder highestNumberFinder = new HighestNumberFinder(); public TopicTopScore[] findTopicHighScores(TopicScores[] array) { if(array.Length == 1) { List<TopicTopScore> topScores = new List<TopicTopScore>(); TopicScores ts = array[0]; int topScore = highestNumberFinder.findHighestNumber(ts.Scores); topScores.Add(new TopicTopScore(ts.TopicName, topScore)); return topScores.ToArray(); } else return Array.Empty<TopicTopScore>(); } }
Line 3 represents the elephant in the code
We already have tests for the
HighestNumberFinder
class, and yes classTopicManager
requiresHighestNumberFinder
. But the coupling between these two classes is such that we cannot testTopicManger
independently ofHighestNumberFinder
. Actually, this is a code smell.we can not substitute out
HighestNumberFinder
, this also is a code smell.We can remove the code smell by injecting
HighestNumberFinder
intoTopicManager
, whenTopicManager
is created.
Modify
TopicManager
as followsremove the creation of
HighestNumberFinder
at line 5Pass an instance of a
HighestNumberFinder
intoTopicManager
as a constructor parameter
public class TopicManager { private HighestNumberFinder highestNumberFinder; public TopicManager( HighestNumberFinder hnf ) { highestNumberFinder = hnf; } public TopicTopScore[] findTopicHighScores(TopicScores[] array) { if(array.Length == 1) { List<TopicTopScore> topScores = new List<TopicTopScore>(); TopicScores ts = array[0]; int topScore = highestNumberFinder.findHighestNumber(ts.Scores); topScores.Add(new TopicTopScore(ts.TopicName, topScore)); return topScores.ToArray(); } else return Array.Empty<TopicTopScore>(); } }
Refactor the tests so that they run. You should not have to change any of the test data.
Tests are a great way of identifying code smells. Highly coupled code leads to untestable code.
You want to test one class and one class only. The previous version of the TopicManager
dragged in HighestNumberFinder
. So inadvertently you were testing that class as well. This will become clearer as we continue to work through the TopicManager
tests.
QLC-2.2) Working with Stubs
A Stub is part of the family of Test Doubles. They are used to ensure that tests focus on the behaviour of the CUT and not its dependents. Test environments should be controlled and predictable. Test Doubles give you that measure of stability and predictability.
A Stub method is one that returns canned results. A canned result is a predefined result. The result can be specific, a range of values, or any value. Also, the parameters into the method can be specific value, a range of values, or any value.
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
namespace TopicManagerService { public class HighestNumberFinder { public int findHighestNumber(int[] values) { return 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
using NUnit.Framework; namespace TopicManagerService { public class TopicManagerTests { [Test] public void find_heighest_score_in_empty_array_return_empty_array() { // Arrange TopicScores[] topicScores = Array.Empty<TopicScores>(); HighestNumberFinder hnf = new HighestNumberFinder(); TopicManager cut = new TopicManager(hnf); TopicTopScore[] expectedResult = Array.Empty<TopicTopScore>(); // Act TopicTopScore[] result = cut.findTopicHighScores(topicScores); // Assert Assert.That(result, Is.EqualTo(expectedResult)); } [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) }; 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); } } }
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
projectnamespace FindHighestNumberService { public interface IHighestNumberFinder { int findHighestNumber(int[] values); } }
Copy HighestNumberFinder.v6 to
HighestNumberFinderFinal
and refactor the class so that it implements the interface aboveusing System; using FindHighestNumberService; namespace FindHighestNumberServiceFinal { public class EmptyArrayException : Exception { public EmptyArrayException(string message):base(message) { } } public class HighestNumberFinder : IHighestNumberFinder { public int findHighestNumber(int[] values) { if (values.Length < 1) throw new EmptyArrayException("Array is empty"); int result = Int32.MinValue; for (int i = 0; i < values.Length; i++) { if (values[i] > result) result = values[i]; } return result; } } }
Refactor the tests so that the
HighestNumberFinder
is instantiated as followsIHighestNumberFinder 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.
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()
[Test] public void find_heighest_score_with_array_of_one_return_array_of_one_using_stub() { // Arrange int[] scores = { 56, 67, 45, 89 }; string topicName = "Physics"; TopicScores[] topicScores = { new TopicScores(topicName, scores) }; IHighestNumberFinder hnf = new FindHighestNumberServiceFinal.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); } }
Modify line 8 to
IHighestNumberFinder hnf = new TopicManagerService.HighestNumberFinder();
The error is because of
TopicManagerTests.HighestNumberFinder
does not implement theIHighestNumberFinder
interface. Modify it so that it does.Rerun the tests, they should all pass.
We’ve now created a controlled environment for our tests
QLC-2.3 Stubs, complete the last two TopicManager requirements
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}]
•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}]
Add Comment