Lettuce (http://lettuce.it) is a Cucumber-like BDD tool built for Python.
Cucumber (http://cukes.info) was developed by the Ruby community and provides a way to write scenarios in a textual style. By letting our stakeholders read the stories, they can easily discern what the software is expected to do.
This recipe shows how to install Lettuce, write a test story, and then wire it into our shopping cart application 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.
Install lettuce by typing pip install lettuce
.
In the following steps, we will explore creating some testable stories with Lettuce, and wiring it to runnable Python code:
recipe32
to contain all the files in this recipe.recipe32.feature
to capture our story. Write the top-level description of our new feature, based on 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.00 And the price with taxes is $0.00
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% taes is $6.05
recipe32
called steps.py
to implement the steps needed to support the Givens.steps.py
to implement the first Given.from lettuce import * from cart import * @step("an empty cart") def an_empty_cart(step): world.cart = ShoppingCart()
cart.py
module is part of our PYTHONPATH.steps.py
that implements support for the first scenario's Thens.@step("looking up the fifth item causes an error") def looking_up_fifth_item(step): try: world.cart.item(5) raise AssertionError("Expected IndexError") except IndexError, e: pass @step("looking up a negative price causes an error") def looking_up_negative_price(step): try: world.cart.price(-2) raise AssertionError("Expected IndexError") except IndexError, e: pass @step("the price with no taxes is (.*)") def price_with_no_taxes(step, total): assert world.cart.total(0.0) == float(total) @step("the price with taxes is (.*)") def price_with_taxes(step, total): assert world.cart.total(10.0) == float(total)
steps.py
to implement the steps needed for the second scenario.@step("I add a carton of milk for (.*)") def add_a_carton_of_milk(step, price): world.cart.add("carton of milk", float(price)) @step("I add another carton of milk for (.*)") def add_another_carton_of_milk(step, price): world.cart.add("carton of milk", float(price)) @step("the first item is a carton of milk") def check_first_item(step): assert world.cart.item(1) == "carton of milk" @step("the price is (.*)") def check_first_price(step, price): assert world.cart.price(1) == float(price) @step("the cart has (.*) items") def check_size_of_cart(step, num_items): assert len(world.cart) == float(num_items) @step("the total cost with (.*)% taxes is (.*)") def check_total_cost(step, tax_rate, total): assert world.cart.total(float(tax_rate)) == float(total)
steps.py
to implement the steps needed for the last scenario.@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): assert world.cart.item(2) == "frozen pizza" @step("the first price is (.*)") def check_the_first_price(step, price): assert world.cart.price(1) == float(price) @step("the second price is (.*)") def check_the_second_price(step, price): assert world.cart.price(2) == float(price) @step("the total cost with no taxes is (.*)") def check_total_cost_with_no_taxes(step, total): assert world.cart.total(0.0) == float(total) @step("the total cost with (.*)% taxes is (.*)") def check_total_cost_with_taxes(step, tax_rate, total): assert round(world.cart.total(float(tax_rate)),2) == float(total)
lettuce recipe32
and see how they are all now passing. In the next screenshot, we have all the tests passing and everything is green.Lettuce uses the popular Given/When/Then style of BDD story telling.
try
-catch
block with a throw
if the expected exception didn't occur.It doesn't matter in what order we put the Given/Then/When. Lettuce will record everything so that all the Givens are listed first, followed by all the Whens, and then all the Thens. Lettuce puts on the final polish by translating successive Given/When/Then into And for better readability.
If you look closely at some of the steps, you will notice some wildcards.
@step("the total cost with (.*)% taxes is (.*)") def check_total_cost(step, tax_rate, total): assert world.cart.total(float(tax_rate)) == float(total)
The @step
string lets us dynamically grab parts of the string as variables by using pattern matchers.
.*
) is a pattern to capture the tax_rate
.*
) is a pattern to capture the total
The method definition shows these two extra variables added in. We can name them anything we want. This gives us the ability to actually drive the tests, data and all, from recipe32.feature
and only use steps.py
to link things together in a generalized way.
In this recipe, we fit everything into a single story. Our story involved all the various shopping cart operations. As we write more scenarios, we may expand this into multiple stories. This goes back to the concept discussed in the Breaking down obscure tests into simple ones section of Chapter 1. If we overload a single scenario with too many steps, it may get too complex. It is better if we can visualize a single thread of execution that is easy to verify at the end.
The project's website shows a sample building a factorial function. It has both the factorial function as well as the wiring in a single file. For demo purposes this is alright. But for actual production use, it is best to decouple the application from the Lettuce wiring. This encourages a clean interface and demonstrates usability.
Breaking down obscure tests into simple ones section from Chapter 1
3.144.97.187