Lettuce (http://lettuce.it) is a BDD tool built for Python.
The Should DSL (http://www.should-dsl.info) provides a simpler way to write assertions for Thens.
This recipe shows how to install Lettuce and Should DSL. Then, we will write a test story. Finally, we will wire it into our shopping cart application using the Should DSL to exercise our code.
For this recipe, we will be using the shopping cart application shown at the beginning of this chapter. We also need to install Lettuce and its dependencies:
With the following steps, we will use the Should DSL to write more succinct assertions in our test stories:
recipe33
to contain all the files for this recipe.recipe33
called recipe33.feature
to contain our test scenarios.recipe33.feature
with several scenarios to exercise our shopping cart.Feature: Shopping cart As a shopper I want to load up items in my cart So that I can check out and pay for them Scenario: Empty cart Given an empty cart Then looking up the fifth item causes an error And looking up a negative price causes an error And the price with no taxes is 0.0 And the price with taxes is 0.0 Scenario: Cart getting loaded with multiple of the same Given an empty cart When I add a carton of milk for 2.50 And I add another carton of milk for 2.50 Then the first item is a carton of milk And the price is 5.00 And the cart has 2 items And the total cost with 10% taxes is 5.50 Scenario: Cart getting loaded with different items Given an empty cart When I add a carton of milk And I add a frozen pizza Then the first item is a carton of milk And the second item is a frozen pizza And the first price is 2.50 And the second price is 3.00 And the total cost with no taxes is 5.50 And the total cost with 10% taxes is 6.05
from lettuce import * from should_dsl import should, should_not from cart import * @step("an empty cart") def an_empty_cart(step): world.cart = ShoppingCart() @step("looking up the fifth item causes an error") def looking_up_fifth_item(step): (world.cart.item, 5) |should| throw(IndexError) @step("looking up a negative price causes an error") def looking_up_negative_price(step): (world.cart.price, -2) |should| throw(IndexError) @step("the price with no taxes is (.*)") def price_with_no_taxes(step, total): world.cart.total(0.0) |should| equal_to(float(total)) @step("the price with taxes is (.*)") def price_with_taxes(step, total): world.cart.total(10.0) |should| equal_to(float(total)) @step("I add a carton of milk for 2.50") def add_a_carton_of_milk(step): world.cart.add("carton of milk", 2.50) @step("I add another carton of milk for 2.50") def add_another_carton_of_milk(step): world.cart.add("carton of milk", 2.50) @step("the first item is a carton of milk") def check_first_item(step): world.cart.item(1) |should| equal_to("carton of milk") @step("the price is 5.00") def check_first_price(step): world.cart.price(1) |should| equal_to(5.0) @step("the cart has 2 items") def check_size_of_cart(step): len(world.cart) |should| equal_to(2) @step("the total cost with 10% taxes is 5.50") def check_total_cost(step): world.cart.total(10.0) |should| equal_to(5.5) @step("I add a carton of milk") def add_a_carton_of_milk(step): world.cart.add("carton of milk", 2.50) @step("I add a frozen pizza") def add_a_frozen_pizza(step): world.cart.add("frozen pizza", 3.00) @step("the second item is a frozen pizza") def check_the_second_item(step): world.cart.item(2) |should| equal_to("frozen pizza") @step("the first price is 2.50") def check_the_first_price(step): world.cart.price(1) |should| equal_to(2.5) @step("the second price is 3.00") def check_the_second_price(step): world.cart.price(2) |should| equal_to(3.0) @step("the total cost with no taxes is 5.50") def check_total_cost_with_no_taxes(step): world.cart.total(0.0) |should| equal_to(5.5) @step("the total cost with 10% taxes is (.*)") def check_total_cost_with_taxes(step, total): world.cart.total(10.0) |should| close_to(float(total), delta=0.1)
The previous recipe (Writing a testable story with Lettuce), shows more details on how Lettuce works. This recipe demonstrates how to use the Should DSL to make useful assertions.
Why do we need Should DSL? The simplest checks we write involve testing values to confirm the behavior of the shopping cart application. In the previous recipe, we mostly used Python assertions.
assert len(context.cart) == 2
This is pretty easy to understand. Should DSL offers a simple alternative.
len(context.cart) |should| equal_to(2)
Does this look like much of a difference? Some say yes, others say no. It is wordier, and for some this is easier to read. For others, it isn't.
So why are we visiting this? Because, Should DSL has more than just equal_to
. There are many more:
be
: check identitycontain
, include
, be_into
: verify if an object is contained or contains anotherbe_kind_of
: check typesbe_like
: checks using a regular expressionbe_thrown_by
, throws
: check that an exception is thrownclose_to
: check if value is close, given a deltaend_with
: check if a string ends with a given suffixequal_to
: check value equalityrespond_to
: check if an object has a given attribute or methodstart_with
: check if a string starts with a given prefixThere are other alternatives as well, but this provides a diverse set of comparisons. If we imagine the code needed to write assertions that check the same things, then things get more complex.
For example, let's think about confirming expected exceptions. In the previous recipe, we needed to confirm that an IndexError
is thrown when accessing an item outside the boundaries of our cart. A simple Python assert
didn't work, so instead we coded this pattern.
try: world.cart.price(-2) raise AssertionError("Expected an IndexError") except IndexError, e: pass
This is clunky and ugly. Now, imagine a more complex, more realistic system, and the idea of having to use this pattern for lots of test situations where we want to verify that proper exception is thrown. This can quickly become an expensive coding task.
Thankfully, Should DSL turns this pattern of exception assertion into a one-liner.
(world.cart.price, -2) |should| throw(IndexError)
This is clear and concise. We can instantly understand that invoking this method with these arguments should throw a certain exception. If no exception is raised, or a different one is raised, it will fail and give us clear feedback.
In the sample code listed in this chapter, we used |should|
. But Should DSL also comes with |should_not|
. Sometimes, the condition we want to express is best captured with a |should_not|
. Combined with all the matchers listed earlier, we have a plethora of opportunities to test things, positive or negative.
But don't forget, we can still use Python's plain old assert
if it is easier to read. The idea is to have plenty of ways to express the same verification of behavior.
3.15.219.130