...
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)
In FindHighestNumber project create a new package that ends with
.topicmanager
Create a new setup the new test class as shown here (remember you will have errors)
Code Block language c# namespace TopicManagerTests { package xx.yy.zz.topicmanagerservice public class TopicManagerTests { @Test public public void findvoid 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 package called TopicManagerService
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 top 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 ArrayList<TopicScores> array = new ArrayList<>(); TopicManager cut = new TopicManager(); ArrayList<TopicTopScore> expectedResult = new ArrayList<>(); // Act ArrayList<TopicTopScore> result = cut.findTopicHighScores( array ); // Assert assertEquals( expectedResult, result ); } }
We are now in a good position to get VS to generate the
findTopicHighScores
with the correct signatureCode Block language c# class TopicManager { ArrayList<TopicTopScore> findTopicHighScores(ArrayList<TopicScores> array) { return new ArrayList<>(); } }
Now we need to complete the first piece of implementation code to pass the test
So you will also need the following pieces of codewill also need the following pieces of code
Code Block package com.s2s.demos.topicmanager; class Topic { private String topicName; int score; public Topic( String topicName, int score) { this.topicName = topicName; this.score = score; } public String getTopicName() { return topicName; } public int getScore() { return score; } }
Code Block public class TopicTopScore { }
Code Block public class TopicScores { }
...
Implement this second test
Code Block language java @Test public void find_heighest_score_with_array_of_one_return_array_of_one() { // Arrange int[] scores = { 56, 67, 45, 89 }; String topicName = "Physics"; ArrayList<TopicScores> topicScores = new ArrayList<>(); topicScores.add(new TopicScores(topicName, scores)); TopicManager cut = new TopicManager(); ArrayList<TopicTopScore> expectedResult = new ArrayList<>(); expectedResult.add(new TopicTopScore(topicName, 89)); // Act ArrayList<TopicTopScore> result = cut.findTopicHighScores(topicScores); assertEquals(expectedResult.get(0).getTopicName(), result.get(0).getTopicName()); assertEquals(expectedResult.get(0).getTopScore(), result.get(0).getTopScore() ); }
And here is the implementation class
Code Block language c#java class TopicManager { private final HighestNumberFinder highestNumberFinder = new HighestNumberFinder(); ArrayList<TopicTopScore> findTopicHighScores(ArrayList<TopicScores> array) { ArrayList<TopicTopScore> topScores = new ArrayList<>(); if(array.size() == 1) { TopicScores ts = array.get(0); int topScore = highestNumberFinder.findHighestNumber(ts.getScores()); topScores.add(new TopicTopScore(ts.getTopicName(), topScore)); } return topScores; } }
Updated TopicScores.java
Code Block language java package com.s2s.demos.topicmanager; public class TopicScores { private String topicName; private int[] scores; public TopicScores(String topicName, int[] scores) { this.topicName = topicName; this.scores = scores; } public String getTopicName() { return topicName; } public int[] getScores() { return scores; } }
Updated TopicTopScores.java
Code Block language java public class TopicTopScore { private String topicName; private int topScore; public TopicTopScore(String topicName, int score) { this.topScore = score; this.topicName = topicName; } public String getTopicName() { return topicName; } public int getTopScore() { return topScore; } }
Looking back at TopicManager,
Line 3
represents the elephant in the codeWe 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
Code Block language c#java 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.
...
Begin by creating a new test
Code Block | ||
---|---|---|
| ||
@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"; ArrayList<TopicScores> topicScores = new ArrayList<>(); topicScores.add(new TopicScores(topicName, scores)); // Use a stub version of HighestNumberFinder com.s2s.demos.topicmanager.HighestNumberFinder hnf = new com.s2s.demos.topicmanager.HighestNumberFinder(); TopicManager cut = new TopicManager(hnf); ArrayList<TopicTopScore> expectedResult = new ArrayList<>(); expectedResult.add(new TopicTopScore(topicName, 89)); // Act ArrayList<TopicTopScore> result = cut.findTopicHighScores(topicScores); assertEquals(expectedResult.get(0).getTopicName(), result.get(0).getTopicName()); assertEquals(expectedResult.get(0).getTopScore(), result.get(0).getTopScore() ); } |
There will be a number of errors in the code, we will eliminate them one by one
Here is the Stub you will use (create it in the test folder - it’s not production code)
Code Block | ||
---|---|---|
| ||
package com.s2s.demos.topicmanager; public class HighestNumberFinder { public int findHighestNumber(int[] array) { return 89; } } |
The stub is substituted in line 1311.
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
, is typed against com.s2s.demos.findhighestnumber.fin.HighestNumberFinder
. So namespaces packages do not offer us the solution we are looking for.
We need to use interfaces. And the interface needs to be in a place that is available to both production code and teststo use interfaces. 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.
Create a new interface that is part of the production code, and create it in the
com.s2s.demos.findhighestnumber
packageCode Block language c#java package com.s2s.demos.findhighestnumber; public interface IHighestNumberFinder { int findHighestNumber(int[] values); }
Modify the production version and test version of HighestNumberFinder, so that they both implement the interface
IHighestNumberFinder
Here is the stub version
Code Block language java package com.s2s.demos.topicmanager; import com.s2s.demos.findhighestnumber.IHighestNumberFinder; // The STUB version public class HighestNumberFinder implements IHighestNumberFinder { @Override public int findHighestNumber(int[] array) { return 89; } }
Here is the production version
Code Block language java package com.s2s.demos.findhighestnumber.fin; import com.s2s.demos.findhighestnumber.IHighestNumberFinder; public class HighestNumberFinder implements IHighestNumberFinder { @Override public int findHighestNumber(int[] array) { int highestSoFar = Integer.MIN_VALUE; for( int val : array ) { if( val > highestSoFar ) highestSoFar = val; } return highestSoFar; } }
Refactor
TopicManager
so it now works with the interfaceIHighestNumberFinder
and not directly with the implementation classCode Block language java package com.s2s.demos.topicmanager; import com.s2s.demos.findhighestnumber.IHighestNumberFinder; import java.util.ArrayList; class TopicManager { private final IHighestNumberFinder highestNumberFinder; public TopicManager() { this.highestNumberFinder = null; } public TopicManager( IHighestNumberFinder hnf ) { highestNumberFinder = hnf; } ...
You should find that the test is no longer in an error state
Rerun all your tests they should still be passing.
...
•If the input is [{“Physics”, {56, 67, 45, 89}}, {“Art”, {87, 66, 78}], the result should be [{“Physics”, 89}, {“Art”, 87}]
One Only one of the results will failtests passes. Why?
QLC-2.4 Mocks, complete the last TopicManager requirement
...