Skip to content

Latest commit

 

History

History
168 lines (107 loc) · 6.65 KB

testing.md

File metadata and controls

168 lines (107 loc) · 6.65 KB

Testing and helpers

Boiler provides as set of flask-specific helpers to ease testing view and database applications. We use nosetests for testing that is fully integrated into project CLI so you can simply run ./cli test to run your tests. But before we do that we need to install some stuff.

Install testing dependencies

This is done with boiler dependencies command:

boiler dependencies testing

This will get you the following packages:

  • nose to ron your tests
  • rednose for nice coloured output
  • faker for generating dummy test data
  • coverage for code coverage reports

When initialized, boiler created a nose.ini file in the root of your project. This is where you can tweak different nose settings like verbosity, coverage generation etc. You can use all nose command line options in this config. However the defaults work quite well out of the box both for staging and production.

Writing an running tests

As mentioned earlier nose is integrated into your project CLI out of the bix, so simple run ./cli test to runn your tests. The command proxies all command-line arguments to nose executable, so all usual nose args work with boiler CLI, for example to run only the tests tagget with certain @attr('some-tag-name') decorator, pass attr name to CLI:

./cli test -a some-tag-name

It is important to mention that nose will discover tests in your project. However please remember that:

  • Test package must contain __init__.py to make discovery possible
  • Test files must end with _test.py, e.g. some_service_test.py
  • Tets case method name must start with test, as in def test_loggin_in(self):

Let's write a simple test:

/project
/tests
    __init__.py
    example_test.py

Create the tests directory with these two files. We are going to write our first test now:

import unittest


class FirstTestEver(unittest.TestCase):
    def test_can_do_testing(self):
        """ Short test description to appear at runtime """
        self.assertTrue(True)

And finally run in with ./cli test

Testing Flask Applications

Most of the time when building a flask app your tests would probably be more complicated than that. Perhaps involving testing flask views, api endpoints or integration testing with ORM, so boiler has a neat set of features to help with that.

You start off by creating a base test case for your project and extending your test cases from that instead of unittest.TestCase. So under your test directory, create a base test case file /tests/base.py

# /tests/base.py
import os
from boiler import testing
from boiler import bootstrap
from backend.config import TestingConfig


class BaseTest(testing.ViewTestCase):
    def setUp(self):
        """ Set up flask app """
        os.environ['FLASK_CONFIG'] = 'backend.config.TestingConfig'
        from backend.app import app
        super().setUp(app=app)

That's it you can now extend your test cases from this base test case. Here we created an app (because, you know, you can have more than one) and chosen to use our testing config. We also extended our base test case from one of base boiler test cases, of whch there are two:

  • FlaskTestCase provides tools to test backend services and integrates with ORM
  • ViewTestCase builds on top of that to provide set of convenience methods and assertions to help with testing views and API responses

FlaskTestCase

This is the main base test case for testing flask apps. It will bootstrap an app and make it available as self.app in case you need access to it. It will also create and push app context and make it available to your tests through self.app_context.

Additionally, FlaskTestCase provides you with methods to manage your test database. Remember that test config we created? It will actually replace your actual database with an SQLite database when testing. This database will be put under /var/data.test.db and loaded with tables from reading metadata from your models. After that a copy of that fresh database will be created to enable fast rollbacks between tests without having to re-read metadata and recreate tables.

The typical workflow when testing with a database is to add self.create_db() to your base setUp method which will also ebale access to database instance via self.db. Boiler will then automatically refresh_db() in the tearDown in between every test.

You can of course manually call self.refresh_db() whenever needed within your tests with an option to force: self.refresh_db(force=True) which will force recreation of tables from metadata, a bit more expensive but sometime usefull operation.

ViewTestCase

Builds on top of base FlaskTestCase and provides additional methods for dealing with requests, responses, json data and provides additional assertions. Here's a list of additional features this base testcase adds, but also have a look at the api:

Helpers:

  • self.get()
  • self.post()
  • self.put()
  • self.delete()
  • self.jget()
  • self.jpost()
  • self.jput()
  • self.jdelete()
  • self.getCookies()

Assetions

  • self.assertFlashes()
  • self.assertStatusCode()
  • self.assertOk()
  • self.assertBadRequest()
  • self.assertUnauthorized()
  • self.assertForbidden()
  • self.assertNotFound()
  • self.assertMethodNotAllowed()
  • self.assertConflict()
  • self.assertInternalServerError()
  • self.assertContentType()
  • self.assertOkHtml()
  • self.assertJson()
  • self.assertOkJson()
  • self.assertBadJson()
  • self.assertCookie()
  • self.assertCookieEquals()
  • self.assertInResponse()
  • self.assertNotInResponse()

Here is an example view test that makes a json get request to the api, receives response, asserts it's a 200, gets back data, decodes it and asserts there is a welcome in response.

    def test_can_hit_api_index(self):
        """ Accessing api index"""
        
        # make request, get response
        resp = self.jget(self.url('/'))
        
        # assert it's a json response with staus 200
        self.assertOkJson(resp)    
        
        # get decoded json as dict            
        data = self.jdata(resp)      
        
        # assert there's a welcome in response
        self.assertIn('welcome', data)