Every developer knows there are times when you just have to play around with some code to see what works and what doesn't. Proof of concept code can be a valuable tool when you're learning something new, trying to figure out a new library, or a plethora of other reasons. Once you've written your proof of concept and it's time to turn into real, quality-driven code, how do you make the transition? This comes up a lot for us so we'll walk you through a few steps that will make sure your proof of concept can seamlessly transition to a test-driven application.
The first thing you'll want to do is take your proof of concept and really look at what it's doing. Think of it like a car: you know it runs, but now you need to really inspect how it runs. You need to look at each individual piece and figure out how they function.
Once you know how the concept code works you'll want to identify any areas that aren't inherently unit-testable. Let's say your proof of concept uses a packaged set of assemblies from a vendor that weren't written to be mocked, spied, etc. You'll want to identify those pieces before you try to start unit testing or it might be bad news later.
Next you'll want to generate some really loose technical requirements. We know, you thought technical requirements were a thing of the past (waterfall, anyone?), but they are sometimes still useful. This is one of those situations. Don't get too specific with them and don't spend a lot of time writing them. Just jot down what's happening in your proof of concept code now so you'll know what tests to write in a few minutes.
At this point you have a working proof of concept, a thorough understanding of how it's working, an awareness of which parts aren't unit-testable, and some really rough technical requirements. You're ready to start your real code now. From this point on you basically get to approach your test-driven process the same way you would if you had not created a proof of concept first. You'll write your tests based on your requirements, write just enough code so the tests can pass, then refactor the code (and the tests) until it all makes you happy. How about an example?
Our feature is a full-fledged card game suite. We ultimately want to have multiple games available where the user can choose which game to play, how much he wants to wager (just for fun!), how many decks of cards to use, and a few other options. The first step is figuring out how to effectively shuffle a deck of cards. No matter what we decide to do with any of the card games, if we can't shuffle the cards we won't create a positive experience for our users. For this sprint (we're agile), one of the stories our team has taken on is developing the card shuffling mechanism that will be used by all of the card games in the future. We decided to write a proof of concept to make sure we have a really solid idea of how we're going to do this (and whether it's even possible) before we start writing our tests and our code.
Now, we want to specify right up front that this isn't necessarily the best way to do this. We're showing how to do it because we know that we don't always get to do things the best way. In those cases we have to accept what we've been given and make the best of it, which is what we're about to do.
Here's what our proof of concept looks like right now:
1: public void Shuffle()
2: {
3: var deck = new Deck { Cards = new List<string>(), IsShuffled = false };
4:
5: while (deck.Cards.Count < 52)
6: {
7: var random = new Random();
8: var position = random.Next(1, 53);
9:
10: if (!deck.Cards.Contains(_deck[position]))
11: {
12: deck.Cards.Add(_deck[position]);
13: }
14: }
15:
16: deck.Cards.ForEach(Console.WriteLine);
17: }
Now we want to go through our process so we can use test-driven development to build the releaseable, reusable shuffle code.
- Figure out how everything works
This is a pretty easy one since our proof of concept code is so simple and small - Identify areas that aren't inherently unit-testable
We're using the System.Random class, which doesn't have an interface that we can mock so we'll have to do something about that.
We're not going to cover how to mock this class in this post, but we'll get into it in the future - Generate loose technical requirements
- Create an empty deck of cards
- Repeat steps 3 and 4 until the new deck is populated with 52 cards
- Generate a random number
- Put the card represented by the random number into the new deck if it isn't already in there
So now we know what's happening in our proof of concept and we can identify what tests we should write, at least for this part of our shuffler. We generated those technical requirements just so we'd really know what our proof of concept is doing, and therefore what our actual code should do. We don't necessarily need to write a separate unit test for each requirement we generated. In fact, we generally shouldn't write one test per acceptance criterion or requirement.
In this case we can probably get away with writing a unit test called ShuffleShouldRandomizeADeckOfCards. This test will check that after we call the Shuffle method we have a deck of cards that is not in consecutive order. Easy enough.
1: [TestMethod]
2: public void ShuffleShouldRandomizeADeckOfCards()
3: {
4: // arrange
5: var shuffler = new Shuffler();
6:
7: // act
8: var deck = shuffler.Shuffle();
9:
10: // assert
11: Assert.AreNotEqual("AH", deck.Cards[0]);
12: }
Depending on what our rules are for testing and what our team thinks, this might be good enough. We also might go with something like this:
1: [TestMethod]
2: public void ShuffleShouldRandomizeADeckOfCards()
3: {
4: // arrange
5: var shuffler = new Shuffler();
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: }
We can make multiple assertions in a single unit test because we're ultimately testing the same thing: whether the deck is shuffled. Remember that at this point our code still looks like this:
1: public Deck Shuffle()
2: {
3: throw new NotImplementedException();
4: }
When our test runs, it will fail. Of course it will, we're not doing anything yet! Since we've achieved a red test, we need to make our test turn green.
1: public Deck Shuffle()
2: {
3: var deck = new Deck
4: {
5: Cards = new List<string>()
6: };
7:
8: deck.Cards.AddRange(new List<string> { "AS", "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "10S", "JS", "QS", "KS" });
9: deck.Cards.AddRange(new List<string> { "AC", "2C", "3C", "4C", "5C", "6C", "7C", "8C", "9C", "10C", "JC", "QC", "KC" });
10: deck.Cards.AddRange(new List<string> { "AD", "2D", "3D", "4D", "5D", "6D", "7D", "8D", "9D", "10D", "JD", "QD", "KD" });
11: deck.Cards.AddRange(new List<string> { "AH", "2H", "3H", "4H", "5H", "6H", "7H", "8H", "9H", "10H", "JH", "QH", "KH" });
12:
13: return deck;
14: }
Now our test passes, but this code isn't shuffling anything. This is probably the hardest part of doing TDD when you already have a proof of concept. We did the proof of concept so we'd know how to do what we want to do, but now we're essentially pretending that we don't know what we want to do. This is tricky, but it's important because by writing this code to turn our test green we've proven that our test isn't very effective. We're going to need some better tests, but remember that our tests shouldn't be more complicated than our code. We'll leave that first test in there for now since we know it passes and we'll add some additional tests, but we need to rename the test since it isn't really checking what the name says. Let's rename it to ShuffleShouldMoveAces. It's not a great name, but it's more descriptive of what's actually going on.
Now we can create another test named ShuffleShouldRandomizeADeckOfCards and do a more extensive check of the randomness of our deck of cards.
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();
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: }
What we're doing here is checking whether each card in the shuffled deck is different than the card in the same position as the unshuffled deck, or the position just before it, or the position just after it. Since we know it's theoretically possible for a shuffled card to end up in the same spot in the deck, we've allowed that to happen with up to four cards and still have a passing test. With the code we have right now, this test fails. Now we can implement our proof of concept code to get make it green.
1: private readonly Dictionary<int, string> _unshuffledDeck =
2: new Dictionary<int, string>
3: {
4: {1, "AH"}, {2, "2H"}, {3, "3H"}, {4, "4H"}, {5, "5H"},
5: { 6, "6H"}, {7, "7H"}, {8, "8H"}, {9, "9H"}, {10, "10H"},
6: { 11, "JH"}, {12, "QH"}, {13, "KH"}, {14, "AD"}, {15, "2D"},
7: { 16, "3D"}, {17, "4D"}, {18, "5D"}, {19, "6D"}, {20, "7D"},
8: { 21, "8D"}, {22, "9D"}, {23, "10D"}, {24, "JD"}, {25, "QD"},
9: { 26, "KD"}, {27, "AC"}, {28, "2C"}, {29, "3C"}, {30, "4C"},
10: { 31, "5C"}, {32, "6C"}, {33, "7C"}, {34, "8C"}, {35, "9C"},
11: { 36, "10C"}, {37, "JC"}, {38, "QC"}, {39, "KC"}, {40, "AS"},
12: { 41, "2S"}, {42, "3S"}, {43, "4S"}, {44, "5S"}, {45, "6S"},
13: { 46, "7S"}, {47, "8S"}, {48, "9S"}, {49, "10S"}, {50, "JS"},
14: { 51, "QS"}, {52, "KS"}
15: };
16:
17: public Deck Shuffle()
18: {
19: var deck = new Deck
20: {
21: Cards = new List<string>(),
22: IsShuffled = false
23: };
24:
25: while (deck.Cards.Count < 52)
26: {
27: var random = new Random();
28: var position = random.Next(1, 53);
29:
30: if (!deck.Cards.Contains(_unshuffledDeck[position]))
31: {
32: deck.Cards.Add(_unshuffledDeck[position]);
33: }
34: }
35:
36: return deck;
37: }
And there we have it*. We took proof of concept code and ended up doing test-driven development with it.
* There is actually a better way to test the randomness of our Shuffle method, but we're going to cover that in a future blog post about the use of wrappers.
No comments:
Post a Comment