Skip to content

Running tests and linting

Your business logic should of course be covered by unit tests. When generating code using lime-project generate the result includes boilerplate tests, where you can add your own tests. For Python we are using pytest and our own pytest plugin lime-test. For Web Components we are using Stencils test framework which uses Jest, and some helpers from @lundalogik/limeclient.js to help create an environment that is similar to a real application.

In Lime projects we are also using linting tools to catch common code mistakes and follow a shared set of formatting rules.
For Python we are using flake8 and for Web Components we are using ESLint.

Info

Our standard CI toolchain will fail any build containing any test or lint error.

Python

Running tests

To run your Python tests with pytest use:

poetry run pytest .

VS Code can be configured to discover and run the tests.

Linting and formatting

flake8

To lint you Python code with flake8 use:

poetry run flake8 .

VS Code can be configured to automatically lint your code.

black

We also recommend formatting the code with black, which will format your code according to a set of formatting rules.

If you have black installed you can either use it to check your code for inconsistencies with regards to the formatting rules, or run the automatic formatting.

Checking your code:

poetry run black . --check

Formatting your code:

poetry run black .

isort

Another recommended formatting tool is isort, which will sort and format the import statements in your python files.

If you have isort installed it can also be used to check your code or run automatic formatting.

Checking your code:

poetry run isort . --check

Formatting your code:

poetry run isort .

The lime-test framework

lime-test helps you write tests for code that makes use of the Lime CRM platform, such as plugins or non-core packages.
It's implemented as a plugin to pytest, which gives you a set of ready made fixtures that you can use for the more basic types of tests for Lime.
For more control, you are encouraged to use the available helper functions that allows you to create more pointed test code.

The following example uses the core_app fixture, which loads a complete LimeApplication (lime_application.application.LimeApplication) with a set of lime types defined that closely matches the current state of the lime-core database.
It also loads a complete database to memory that is disregarded after the test ends to ensure that multiple tests don't interfere with each other.

The code we want to test:

def average(limeobjects, propname):
    values = [o.get_property(propname).value for o in limeobjects]
    return sum(values) / len(values)

The code we can use to test it:

def test_average(core_app):
    Deal = core_app.limetypes.deal
    deals = [
        Deal(value=2),
        Deal(value=4)
    ]
    assert average(deals, 'value') == 3

Fixtures

lime-test is implemented as a plugin to pytest, which means that as soon as it's installed, it will contain fixtures to be used to set up the conditions for your tests more quickly. These fixtures are more often than not to be considered a starting point, and you'll most likely find yourself in need of more fine grained control over your test setups.

To use a fixture in your tests, do this:

def test_my_app(core_app):
    uow = core_app.unit_of_work()
    uow.add(core_app.limetypes.company(name='acme'))
    uow.commit()

    assert len(list(core_app.limetypes.company.get_all())) == 1

Defining data structures

In order for your tests to be manageable, it's essential to distinctly be able to define and read what types are expected of your code. To help with this, lime-test makes use of lime-core's ability to express data structures as dicts or YAML-documents.

The following shows an example of a fixture that sets up an empty database with a coworker and a company that relates to each other. It is recommended to use a separate YAML file for the database structure so it can be reused for testing web components in the frontend.

dsl.yaml
company:
    name: string
    rating:
        type: option
        options:
            - poor
            - good
            - excellent
    responsible:
        type: belongsto
        related: coworker
        backref: companies
coworker:
    company:
        type: hasmany
        related: company
        backref: responsible
@pytest.fixture
def my_database(empty_database):
    with open("dsl.yaml", "r") as file:
        types = file.read()

    limetypes = lime_type.create_limetypes_from_dsl(types)
    lime_test.db.add_limetypes(empty_database, limetypes)
    return empty_database

For more detailed information see lime_type.create_limetypes_from_dsl. It's also possible to define object instances to use in your tests by using lime_type.create_limeobjects_from_dsl.

In-memory Web Application

lime-test enables you to start up a complete web application in memory that you can make requests to to ensure that your endpoints work as they should. It builds on the in memory database to create a complete environment for your endpoints that is created for your specific test method, and teared down immediately afterwards.

It's an implementation of Flasks's test server

A simple example

Given the following endpoint:

class DealAverage(webserver.LimeResource):
    def get(self, companyid):
        company = self.application.limetypes.company.get(companyid)
        avg = averagelib.average(company.properties.deal.fetch(), 'value')

        return {'average': avg}


api.add_resource(DealAverage, '/dealaverage/<int:companyid>')

We can write the following test:

def test_calculate_average_for_deals(webapp, limeapp):
    Company = limeapp.limetypes.company
    Deal = limeapp.limetypes.deal

    uow = limeapp.unit_of_work()

    acme = Company(name='acme')
    acme_idx = uow.add(acme)

    deals = [
        Deal(name='deal1', value=42),
        Deal(name='deal2', value=128),
    ]
    for d in deals:
        acme.properties.deal.attach(d)
        uow.add(d)

    res = uow.commit()

    acme = res[acme_idx]

    res = webapp.get('/myapp/acme_solution/dealaverage/{acme.id}'
                     .format(acme=acme))

    json_response = json.loads(res.data.decode('utf-8'))
    assert res.status_code == 200
    assert json_response == {'average': 85.0}