Wednesday, August 10, 2016

Getting Started with Behavior Testing in Python with Behave_part2 (end)

Writing Tableized Tests

Often when writing tests we want to test the same behavior against many different parameters and check the results. Behave makes this easier to do by providing tools to create a tableized test instead of writing out each test separately. The next game logic to test is that the dealer knows the point value of its hand. Here is a test that checks several scenarios:
  1. Scenario Outline: Get hand total
  2.   Given a <hand>
  3.   When the dealer sums the cards
  4.   Then the <total> is correct
  5.   Examples: Hands
  6.   | hand          | total |
  7.   | 5,7           | 12    |
  8.   | 5,Q           | 15    |
  9.   | Q,Q,A         | 21    |
  10.   | Q,A           | 21    |
  11.   | A,A,A         | 13    |
You should recognize the familiar "given, when, then" pattern, but there's a lot of differences in this test. First, it is called a "Scenario Outline". Next, it uses parameters in angle brackets that correspond to the headers of the table. Finally, there's a table of inputs ("hand") and outputs ("total").

The steps will be similar to what we've seen before, but we'll now get to use the parameterized steps feature of Behave.

Here's how to implement the new "given" step:
  1. @given('a {hand}')
  2. def step_impl(context, hand):
  3.     context.dealer = Dealer()
  4.     context.dealer.hand = hand.split(',')
The angle brackets in the dealer.feature file are replaced with braces, and the hand parameter becomes an object that is passed to the step, along with the context.

Just like before, we create a new Dealer object, but this time we manually set the dealer's cards instead of generating them randomly. Since the hand parameter is a simple string, we split the parameter to get a list.

Next, add the remaining steps:
  1. @when('the dealer sums the cards')
  2. def step_impl(context):
  3.     context.dealer_total = context.dealer.get_hand_total()
  4. @then('the {total:d} is correct')
  5. def step_impl(context, total):
  6.     assert (context.dealer_total == total)
The "when" step is nothing new, and the "then" step should look familiar. If you're wondering about the ":d" after the total parameter, that is a shortcut to tell Behave to treat the parameter as an integer. It saves us from manually casting with the int() function. Here's a complete list of patterns that Behave accepts and if you need advanced parsing, you can define your own pattern.

There's many different approaches to summing values of cards, but here's one solution to find the total of the dealer's hand. Create this as a top-level function in the twentyone.py module:
  1. def _hand_total(hand):
  2.     values = [None, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 10]
  3.     value_map = {k: v for k, v in zip(_cards, values)}
  4.     total = sum([value_map[card] for card in hand if card != 'A'])
  5.     ace_count = hand.count('A')

  6.     for i in range(ace_count, -1, -1):
  7.         if i == 0:
  8.             total = total + ace_count
  9.         elif total + (i * 11) + (ace_count - i) <= 21:
  10.             total = total + (i * 11) + ace_count - i
  11.             break
  12.     return total
In short, the function maps the card character strings to point values, and sums the values. However, aces have to be handled separately because they can value 1 or 11 points.

We also need to give the dealer the ability to total its cards. Add this function to the Dealer class:
  1. def get_hand_total(self):
  2.     return _hand_total(self.hand)
If you run behave now, you'll see that each example in the table runs as its own scenario. This saves a lot of space in the features file, but still gives us rigorous tests that pass or fail individually.

We'll add one more tableized test, this time to test that the dealer plays by the rules. Traditionally, the dealer must play "hit" until he or she has 17 or more points. Add this scenario outline to test that behavior:
  1. Scenario Outline: Dealer plays by the rules
  2.   Given a hand <total>
  3.    when the dealer determines a play
  4.    then the <play> is correct

  5.   Examples: Hands
  6.   | total  | play   |
  7.   | 10     | hit    |
  8.   | 15     | hit    |
  9.   | 16     | hit    |
  10.   | 17     | stand  |
  11.   | 18     | stand  |
  12.   | 19     | stand  |
  13.   | 20     | stand  |
  14.   | 21     | stand  |
  15.   | 22     | stand  |
Before we add the next steps, it's important to understand that when using parameters, the order matters. Parameterized steps should be ordered from most restrictive to least restrictive. If you do not do this, the correct step may not be matched by Behave. To make this easier, group your steps by type. Here is the new given step, ordered properly:
  1. @given('a dealer')
  2. def step_impl(context):
  3.     context.dealer = Dealer()
  1. ## NEW STEP
  2. @given('a hand {total:d}')
  3. def step_impl(context, total):
  4.     context.dealer = Dealer()
  5.     context.total = total
  1. @given('a {hand}')
  2. def step_impl(context, hand):
  3.     context.dealer = Dealer()
  4.     context.dealer.hand = hand.split(',')
The typed parameter {total:d} is more restrictive than the untyped {hand}, so it must come earlier in the file.

The new "when" step is not parameterized and can be placed anywhere, but, for readability, should be grouped with the other when steps:
  1. @when('the dealer determines a play')
  2. def step_impl(context):
  3.     context.dealer_play = context.dealer.determine_play(context.total)
Notice that this test expects a determine_play() method, which we can add to the Dealer class:
  1. def determine_play(self, total):
  2.     if total < 17:
  3.         return 'hit'
  4.     else:
  5.         return 'stand'
Last, the "then" step is parameterized so it needs to also be ordered properly:
  1. @then('the dealer gives itself two cards')
  2. def step_impl(context):
  3.     assert (len(context.dealer.hand) == 2)
  1. @then('the {total:d} is correct')
  2. def step_impl(context, total):
  3.     assert (context.dealer_total == total)
  1. ## NEW STEP
  2. @then('the {play} is correct')
  3. def step_impl(context, play):
  4.     assert (context.dealer_play == play)
Putting Everything Together

We're going to add one final test that will tie together all of the code we've just written. We've proven to ourselves with tests that the dealer can deal itself cards, determine its hand total, and make a play separately, but there's no code to tie this together. Since we are emphasizing test-driven development, let's add a test for this behavior.
  1. Scenario: A Dealer can always play
  2.   Given a dealer
  3.   When the round starts
  4.   Then the dealer chooses a play
We already wrote steps for the "given" and "when" statements, but we need to add a step for "the dealer chooses a play." Add this new step, and be sure to order it properly:
  1. @then('the dealer gives itself two cards')
  2. def step_impl(context):
  3.     assert (len(context.dealer.hand) == 2)
  1. #NEW STEP
  2. @then('the dealer chooses a play')
  3. def step_impl(context):
  4.     assert (context.dealer.make_play() in ['stand', 'hit'])
  1. @then('the {total:d} is correct')
  2. def step_impl(context, total):
  3.     assert (context.dealer_total == total)
This test relies on a new method make_play() that you should now add to the Dealer class:
  1. def make_play(self):
  2.     return self.determine_play(self.get_hand_total())
This method isn't critical, but makes it easier to use the Dealer class.

If you've done everything correctly, running behave should display all of the tests and give a summary similar to this:
  1. 1 feature passed, 0 failed, 0 skipped
  2. 16 scenarios passed, 0 failed, 0 skipped
  3. 48 steps passed, 0 failed, 0 skipped, 0 undefined
  4. Took 0m0.007s
Conclusion

This tutorial walked you through setting up a new project with the Behave library and using test-driven development to build the code based off of behavioral tests.

If you would like to get experience writing more tests with this project, try implementing a Player class and player.feature that plays with some basic strategy.
Written by Phillip Johnson

If you found this post interesting, follow and support us.
Suggest for you:

Build Your First Python and Django Application

Zero to Hero with Python Professional Python Programmer Bundle

The Python Mega Course: Build 10 Python Applications

Complete Python Bootcamp (Hot)

Learning Python for Data Analysis and Visualization



No comments:

Post a Comment