tushman.io

The musings of an insecure technologist

Parallelize Your Lettuce Tests to Win Friends and Influence Others

tl;dr: I forked the lettuce package to use multiprocessing, tests run more then 4x faster on my MBP

I am a fan of Gabriel Falcão’s lettuce Behavior-Driven Development (BDD) tool. We have been using it on my team for 6+ months now. Recently our test suite completion time has crossed the 10 minute line, which had a bunch of negative effects, as you can imagine:

  • people writing less tests
  • people running the test suite less frequently
  • people spending more time watching a test suite run, then coding, …

We all are using relatively modern MBP with 4 cores, and we might as well make the most of them. Here is my fork of lettuce that allows you to take advantage of all of your cores:

https://github.com/jtushman/lettuce

I have made two main modifications (You will find the lion share of my modifications in this file):

  • I created a ParallelRunner (I have left the main runner alone), which kicks off processes to pull the scenarios off a queue
  • After each run I store the run times of each test in a .scenarios file, so in subsequent runs I can sort them longest to shortest

My test suite used to take 12 minutes, now its takes 2 minutes — REJOICE!

Usage

lettuce tests -p 4 -v 2

-p: stands for parallel. You can set it to how many processes you like, I find that the number of cores should be your default

-v is the same verbosity parameter, but I recommend setting it to 2 when using parallelization, otherwise the steps will interlace and not make much sense

in your terrain.py file, there are two new callbacks:

@before.batch and @after.batch

which you should use to set up and tear down each process. I use main to fire up flask, selenium and mongo. Also note that I set a port_number attribute on world which you can use set up processes specific servers. For example:

1
2
3
4
5
6
@before.batch
def batch_setup():
    settings.MONGO_DATABASE_NAME = 'testing__{}'.format(world.port_number)
    mongoengine.connect(settings.MONGO_DATABASE_NAME, host=settings.MONGO_HOST, port=settings.MONGO_PORT,
                        username=settings.MONGO_USERNAME, password=settings.MONGO_PASSWORD)
    clear_database()

Caveats

For this to work all of your tests need to be isolated, they can not depend on each other (which I think is best practice anyways). This means in your tests you should not use world at all Use scenario instead:

To do this, in your terrain file add the following:

1
2
3
4
5
6
7
8
class ScenarioState(object): pass

@before.each_scenario
def setup_senario(senario):
    world.scenario = ScenarioState()

def scenario():
    return world.scenario

And I use this all the time in my steps to refer to state from previous steps

1
2
3
4
5
6
7
8
@step(u'Given a user exists with one account')
def given_a_user_exists(step):
    scenario.current_user = UserFactory.create()

@step(u'And the user has a dog')
def user_has_a_dog(step):
    scenario.current_user.dog = DogFactory.create()

Hope you guys find this useful!

Comments