Carinata
A (rough-scaled) python spec runner
Carinata is a python library which transforms spec files into unittest cases. It tries to be a bit like RSpec, but for python
Spec files contain blocks called describe
, context
, before
, after
,
let
and it
, which in turn contain pure python. Carinata uses these blocks
to create a TestCase
corresponding to each it
block, with the setup from
before
and let
and the teardown from after
.
Installation
As usual (you’re using a virtualenv right?), either do
$ pip install carinata
or grab the latest code from GitHub and install that
$ git clone https://github.com/scottmcginness/carinata.git
$ cd carinata
$ python setup.py install
Usage
From a directory containing a bunch of spec files (extension .carinata
)
$ carinata .
As a Django management command,
$ ./manage.py spec someapp
See carinata --help
for a few more options.
Examples
Suppose you need to test your simple class:
class AwesomeClass(object):
def __init__(self, operator, arg):
self.operator = operator
self.arg = arg
def do_it(self):
return operator(self.arg, 2)
def get_it(self):
return operator[self.arg]
You might test it like this:
from unittest import TestCase
from mymodule import AwesomeClass
import operator
describe "AwesomeClass":
before "each test":
do_something_maybe_involving_a_database('?')
let "get_awesome": lambda: AwesomeClass(self.operator, self.arg)
context "with a multiplier and a 3":
let "operator": operator.mul
let "arg": 3
it "returns a 6 when you do it":
awesome = self.get_awesome()
assert awesome.do_it() == 6
it "returns a 9 when you bop it with a 3":
awesome = self.get_awesome()
assert awesome.bop_it(2) == 8
context "with a dictionary and an 'a'":
let "operator":
return {'a': 1}
let "arg": 'a'
it "returns 1 when you get it":
awesome = self.get_awesome()
assert awesome.get_it() == 1
This will generate regular unittest.TestCase
s from the describe
,
consisting of tests like test_returns_1_when_you_get_it()
in a class
called TestAwesomeClassWithADictionaryAndAnA
.
The advantages with this approach include:
- Self-documented tests, in plain english
- Shared contexts and setup code, without copying and pasting
- Your code is still just python
Some things to note:
- The
before
,after
andlet
blocks are evaluated top-down. They are each put in the test’ssetUp()
ortearDown()
, andlet
blocks are assigned as attributes ofself
. Eachbefore
andafter
may be thought of as just a partial setup or teardown. - That is why we have a top level
lambda
which gets an awesome class: the argumentsself.operator
andself.arg
are not defined until we hit thecontext
blocks. - The
let
blocks can be defined without areturn
, but only if it is a single line (sort of as if they were alambda
). Butreturn
is required if they are more than one line, just like functions. - The test class that’s generated for multiple sibling
it
blocks is a single class, as you would expect. - The class to inherit test classes from is called
TestCase
. This needs to be imported somewhere before the firstdescribe
in each file. Remember, you can use any class for this, using animport ... as TestCase
. - You can throw in arbitrary code (so long as it’s indented correctly) under
describe
andcontext
and it will be picked up as class code. This may be useful if you require class attributes, likefixtures = ['myfix']
.
Django
All the above comes in useful when you want to test your Django app. The following uses factory_boy to help generate models and python-faker to generate test data (you don’t have to, of course, but they do make your life easier).
Given the following model, and factory to describe it:
# models.py
from django.db import models
from django.contrib.auth.models import User
class BlogPost(models.Model):
user = models.ForeignKey(User)
slug = models.CharField(max_length=60)
title = models.CharField(max_length=60)
body = models.TextField(null=False, blank=False)
def save(self, *args, **kwargs):
if not self.slug.endswith("-suffix"):
self.slug += "-suffix"
super(BlogPost, self).save(*args, **kwargs)
def get_absolute_url(self):
return "/posts/{0}".format(self.pk)
# factories.py
import factory
from faker.lorem import words, paragraphs
from myapp.models import *
class UserFactory(factory.DjangoModelFactory):
FACTORY_FOR = User
# Some definitions for User model
# ...
class BlogPostFactory(factory.DjangoModelFactory):
FACTORY_FOR = BlogPost
user = factory.SubFactory(User)
slug = factory.LazyAttribute(lambda b: "-".join(words(4)))
title = factory.LazyAttribute(lambda b: " ".join(words(4)).title())
body = factory.LazyAttribute(lambda b: "\n".join(paragraphs(5)))
you might test it like this:
from myapp.spec.factories import *
from django.test import TestCase
describe "BlogPost":
let "user": UserFactory()
context "with valid attributes":
let "blog_post": BlogPost.build()
it "saves a suffix on the slug":
assert not self.blog_post.slug.endswith("-suffix")
self.blog_post.save()
assert self.blog_post.slug.endswith("-suffix")
it "has URL /posts/pk":
self.blog_post.save()
pk = self.blog_post.pk
expected = "/posts/{0}".format(pk)
actual = self.blog_post.get_absolute_url()
assert actual == expected
context "with a blank body":
let "blog_post": BlogPost.build(body="")
it "fails to save":
try:
self.blog_post.save()
except:
pass
else:
assert False, "Blog post saved without a body"
With this short toy example (admittedly with some factory boilerplate) we can read and write the tests easily. I recommened using sure if you want those asserts to be less painful. Then you’ll get something even more RSpec‐like.
In the end, carinata will generate a file myapp/tests/models.py
, something like:
from myapp.spec.factories import *
from django.test import TestCase
class TestBlogPostWithValidAttributes(TestCase):
def _set_up_user(self):
return UserFactory()
def _set_up_blog_post(self):
return BlogPost.build(user=self.user)
def setUp(self):
self.user = self._set_up_user()
self.blog_post = self._set_up_blog_post()
def test_saves_a_suffix_on_the_slug(self):
assert not self.blog_post.slug.endswith("-suffix")
self.blog_post.save()
assert self.blog_post.slug.endswith("-suffix")
def test_has_url_posts_pk(self):
self.blog_post.save()
pk = self.blog_post.pk
expected = "/posts/{0}".format(pk)
actual = self.blog_post.get_absolute_url()
assert actual == expected
class TestBlogPostWithABlankBody(TestCase):
def _set_up_user(self):
return UserFactory()
def _set_up_blog_post(self):
return BlogPost.build(body="", user=self.user)
def setUp(self):
self.user = self._set_up_user()
self.blog_post = self._set_up_blog_post()
def test_fails_to_save(self):
try:
self.blog_post.save()
except:
pass
else:
assert False, "Blog post saved without a body"
Comparing the two, there is a little less setup to do in our spec file.
All that’s needed is to run ./manage.py spec myapp
and you should get
the standard tests generated and run.
If you want to be slightly fancier about it,
django-nose with the
pinocchio
spec plugin are also recommended. Once you’ve pip install
ed those, your
settings.py
should contain:
INSTALLED_APPS = (
'myapp',
# ... the usual stuff ...
'carinata',
'django_nose',
'pinocchio',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ('--with-spec', '--spec-color')
NOSE_PLUGINS = ('pinocchio.spec.Spec',)
Running ./manage.py spec myapp
, and you’ll get lovely colored readable test output.
Decorators
You may apply decorators to describe
and context
blocks (where they will be applied
to the generated class) and it
blocks (where they will be applied to the test method.
For example:
@my_class_helper
describe "MyClass":
@my_inner_class_helper
context "with some condition":
@test_method_decorator
it "can be tested":
assert True
context "without that condition":
@test_method_decorator
it "can also be tested":
assert True
which generates two classes with their respective decorators:
@my_class_helper
@my_inner_class_helper
class MyClassWithSomeCondition(TestCase):
@test_method_decorator
def test_can_be_tested(self):
assert True
@my_class_helper
class MyClassWithoutThatCondition(TestCase):
@test_method_decorator
def test_can_also_be_tested(self):
assert True
This comes in useful when using features of mock,
such as patch
ing. Since this can also change the function signature, you may specify
a custom set of arguments for your it
lines, like this:
import mock
describe "Mocking":
@mock.patch('my_module.log')
it "can be useful" (self, log):
assert not log.call_args_list
Authors
Scott McGinness
License (GPL version 3)
Copyright (C) 2013 Scott McGinnes <mcginness.s@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.