If you follow the blog you may recall we discussed the best way to use test-driven development when you've written a proof of concept.
At the end of that post we mentioned there is a better way to test the randomness of the shuffler and we'd discuss it in a future post. Well, here's the future post where we'll discuss it.
The randomization of our shuffler is based on the use of the Random class provided by .NET. Since the Random class doesn't have an interface we can't create a fake object that represents it. That means the Random class is a non-fakeable dependency of our Shuffler class. In this particular case it isn't really a problem to just allow the Random class itself to be called because there are no side effects of such a call. Imagine if we were trying to test a method that writes a file to the operating system, though. We definitely wouldn't want to let that call happen every time our test ran because even in the best case scenario it means we have to add clean up to delete the file when the test is finished. If you consider that tests can run in parallel and test accounts on build servers often don't have I/O permissions you can easily see how it wouldn't be a good idea at all to allow new files to be created during testing.
So what do we do about it? We definitely need a way to create fake objects to use in place of these real non-fakeable objects, but how? That's where wrappers can come into play. We can create a wrapper around the non-fakeable code, inject our wrapper, and use the injected wrapper instead of the underlying code. In the earlier blog post on proof of concepts we said that one of the steps was to identify inherently non-testable code. This is why. If we know about these types of limitations before we start writing our code (and our tests) we can account for them before we do anything else so we don't have to come back and do it later.
OK, we're using Moq to create our fake, injectable Random wrapper (remember that the term "fake objects" here can refer to mocks, stubs, fakes, spies, and dummy objects). In our test we're going to want to know how many times the Random wrapper was called so we're going to want a mock object. We're not going to dive into dependency injection options right now, but we are going to inject our wrapper as a dependency.
Since we're doing test-driven development we'd add the Moq NuGet package first so we can start writing our tests. Since we're only using Moq for testing we only need to add it to the CardGames.Shuffler.Logic.Tests project.
Right now we have a unit test that checks whether the cards are shuffled by iterating through the new deck of cards and checking whether the card in each position is different than the value of the card in the same position of the original (unshuffled) deck. We also check whether the value of the card is different than the value of the card one position before and one position after the same position in the original deck. That's useful, but we had to build in a tolerance to account for the possibility that the card some cards could have ended up in the same position after shuffling that they were in before shuffling. Since shuffling is random, this is a real (albeit slim) possibility.
What we want to do is write a unit test that can check to see how many times the Random class was called. We know from our proof of concept that we should call the Next method of the Random class at least 52 times to shuffle the deck. We also know that when we call the Next method we should pass in 1 and 53 as the parameters every time. We'll call the unit test ShuffleShouldGetAtLeast52RandomNumbersBetween1And53 and all we'll do is check whether we call the Next method at least 52 times.
1: [TestMethod]
2: public void ShuffleShouldGetAtLeast52RandomNumbersBetween1And53()
3: {
4: // arrange
5: var randomResult = 0;
6: var moqRandomWrapper = new Mock<IWrapRandom>();
7: moqRandomWrapper.Setup(p => p.Next(It.IsAny<int>(), It.IsAny<int>()))
8: .Callback<int, int>((minValue, maxValue) => {
9: randomResult++;
10: }).Returns(() => randomResult);
11: var shuffler = new Shuffler(moqRandomWrapper.Object);
12:
13: // act
14: shuffler.Shuffle();
15:
16: // assert
17: moqRandomWrapper.Verify(p => p.Next(1, 53), Times.AtLeast(52));
18: }
We're using Moq to create a mock implementation of a new interface called IWrapRandom. We're then configuring Moq to spy on any calls to the Next method of our interface (we're not spying on the actual Next method of the Random class). The assertion in our test is simply to check whether we generated at least 52 random numbers. The problem at this point, of course, is that we don't actually have
1: public interface IWrapRandom
2: {
3: void Create();
4:
5: int Next(int minValue, int maxValue);
6: }
Our interface is really simple because we don't need very much right now. All we need to be able to do in our test is check whether we've called the Next method at least 52 times. In order to do that we need to be able to create an instance of the Random class (void Create()) and we need to have a Next method. Now we need to modify the Shuffler class to accept an implementation of IWrapRandom in its constructor and then we'll create a private, read-only field to hold the injected implementation of IWrapRandom.
1: private readonly IWrapRandom _randomWrapper;
2:
3: private Shuffler(IWrapRandom randomwWrapper)
4: {
5: _randomWrapper = randomwWrapper;
6: }
Now that our Shuffler has an implementation of IWrapRandom we need to change our code to use it instead of the actual Random class.
1: public Deck Shuffle()
2: {
3: var deck = new Deck
4: {
5: Cards = new List<string>(),
6: IsShuffled = false
7: };
8:
9: while (deck.Cards.Count < 52)
10: {
11: var position = _randomWrapper.Next(1, 53);
12:
13: if (!deck.Cards.Contains(_unshuffledDeck[position]))
14: {
15: deck.Cards.Add(_unshuffledDeck[position]);
16: }
17: }
18:
19: return deck;
20: }
Now we're using the injected implementation of IWrapRandom in our actual code and our new test should pass. Unfortunately, when we added the constructor that accepts IWrapRandom we broke our other two tests. For the moment, go ahead and comment those tests out so we can forge ahead and make sure our latest test passes.
Once we prove that our latest test actually does pass, we need to refactor our two existing tests so they compile, then make sure that all three tests pass together. Remember that unless we change functionality intentionally, our entire test suite should always pass. Here's what our test class looks like after we refactor it.
1: private Mock<IWrapRandom> _moqRandomWrapper;
2:
3: [TestInitialize]
4: public void Setup()
5: {
6: _moqRandomWrapper = new Mock<IWrapRandom>();
7:
8: var random = new Random();
9: _moqRandomWrapper.Setup(p => p.Next(It.IsAny<int>(), It.IsAny<int>()))
10: .Returns(() => random.Next(1, 53));
11: }
We added a new method called Setup that will run before each test in this test class. This allows us to reconfigure the mock of IWrapRandom before each test executes. We're setting up our mock object to use the actual Random class to generate and return random numbers. We have to do this so that our tests pass. If we don't configure the mock object to return a number then our code will fail when it tries to get the card from the unshuffled deck.
1: [TestMethod]
2: public void ShuffleShouldMoveAces()
3: {
4: // arrange
5: var shuffler = new Shuffler(_moqRandomWrapper.Object);
6:
7: // act
8: var deck = shuffler.Shuffle();
9:
10: // assert
11: Assert.AreNotEqual("AH", deck.Cards[0]);
12: Assert.AreNotEqual("AD", deck.Cards[13]);
13: Assert.AreNotEqual("AC", deck.Cards[26]);
14: Assert.AreNotEqual("AS", deck.Cards[39]);
15: }
1: [TestMethod]
2: public void ShuffleShouldRandomizeADeckOfCards()
3: {
4: // arrange
5: var unshuffledDeck = new List<string>
6: {
7: "AH", "2H", "3H", "4H", "5H", "6H", "7H", "8H", "9H", "10H",
8: "JH", "QH", "KH", "AD", "2D", "3D", "4D", "5D", "6D", "7D",
9: "8D", "9D", "10D", "JD", "QD", "KD", "AC", "2C", "3C", "4C",
10: "5C", "6C", "7C", "8C", "9C", "10C", "JC", "QC", "KC", "AS",
11: "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "10S", "JS",
12: "QS", "KS"
13: };
14: var shuffler = new Shuffler(_moqRandomWrapper.Object);
15:
16: // act
17: var deck = shuffler.Shuffle();
18:
19: // assert
20: var numberOfMatches = 0;
21: for (var i = 0; i < 52; i++)
22: {
23: if (unshuffledDeck[i] == deck.Cards[i] ||
24: (i > 0 && deck.Cards[i - 1] == unshuffledDeck[i]) ||
25: (i < 51 && deck.Cards[i + 1] == unshuffledDeck[i]))
26: {
27: numberOfMatches++;
28: }
29: }
30: Assert.IsTrue(numberOfMatches < 5,
31: $"{numberOfMatches} is not less than 5");
32: }
We need to modify the existing tests to inject the mock implementation of IWrapRandom in our constructor.
1: [TestMethod]
2: public void ShuffleShouldGetAtLeast52RandomNumbersBetween1And53()
3: {
4: // arrange
5: var shuffler = new Shuffler(_moqRandomWrapper.Object);
6:
7: // act
8: shuffler.Shuffle();
9:
10: // assert
11: _moqRandomWrapper.Verify(p => p.Next(1, 53), Times.AtLeast(52));
12: }
Finally, we refactor our new test to stop creating a new mock of IWrapRandom and just go ahead and inject the one we're creating in the Setup method (which, remember, is already configured so we don't have to do it again here).
At this point our tests are successful, but if we were to actually run our code we'd have issues. That's because we haven't written an actual implementation of the IWrapRandom interface. We need to specify what should happen during a normal execution of our program. This is an important part of creating wrappers: our wrappers should wrap the smallest amount of code possible. Our real implementation of IWrapRandom looks like this.
1: public class RandomWrapper : IWrapRandom
2: {
3: private Random _random;
4:
5: public void Create()
6: {
7: _random = new Random();
8: }
9:
10: public int Next(int minValue, int maxValue)
11: {
12: return _random.Next(minValue, maxValue);
13: }
14: }
We don't want to put any logic in our methods because we can't test whatever logic we do put in there. Since the whole point of writing the wrapper in the first place was to be able to test whether and how many times the Next method was called, it wouldn't make sense to then put non-testable code in the implementation of the wrapper. Again, this is a really important part of wrappers. Try to keep your wrapper method focused on wrapping a single method of a non-fakeable dependency (the Next method of our wrapper only wraps the Next method of the built-in Random class).
Ideally, all code would be fakeable, but we don't code in an ideal world so sometimes we need to make small changes like this so we can verify our code works the way it's supposed to at all times.
No comments:
Post a Comment