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.

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 lime_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(lime_app):
    Deal = lime_app.limetypes.deal
    deals = [
        Deal(value=2),
        Deal(value=4)
    ]
    assert average(deals, 'value') == 3

Fixtures

This list contains description of fixtures that are used in lime-core and can be used in any dependant project. There is no need to import them. They are available in the global scope. Command pytest --fixtures outputs all available fixtures. Some of them are not described below and one can use them at their own risk!


lime_types

Scope: session

Returns list of limetypes List[LimeType] that are created from DSL.


database

Scope: function

Depends on: lime_types

Returns LimeDatabase like below.

LimeDatabase(
    limename="Lime_DB",
    sqlname="Lime_SQL",
    sqlhost="localhost",
    is_default_database=True,
    id="!!",
)

The database is prepopulated. It contains groups Administrators and Users. It also has users admin (password admin) that belongs to Administrators group, and standard (password standard) that belongs to Users group. Prepopulated groups and users can be accessed from a test. The only active and system language is en_us.

Note: every new user and group created in this database will have identifier greater than 1000, what reflects behavior on production.


connection

Scope: function

Depends on: database

Returns active connection to the database used for tests.


lime_databases

Scope: function

Depends on: database

Returns LimeDatabases that contain the database instance used for tests. The output is the same as for lime_database.get_lime_databases().


lime_app

Scope: function

Depends on: lime_types, database

Requires: admin user in the database.

Returns LimeApplication with user admin that has also session created. Rest of parameters are default. There is no coworker, so user of the fixture must extend it, if needed:

@pytest.fixture
def lime_app(lime_app, save_lime_objects):
    save_lime_objects(lime_app.limetypes.coworker(username=lime_app.user_id))
    return lime_app


lime_app_non_admin

Scope: function

Depends on: lime_types, database

Requires: standard user in the database.

Same as lime_app, but the user is standard.


user_repository

Scope: function

Depends on: database

Yields LimeUsers repository with active connection.


group_repository

Scope: function

Depends on: database

Yields Groups repository with active connection.


create_user

Scope: function

Depends on: database

Factory fixture that creates a user in the database. Example:

def test_something(create_user):
    user = create_user(username="my_user", _return_entity=True)

_return_entity flag (which is False by default) controls whether full LimeUser object is returned (True), or just its identifier (False). Returning identifier is faster and should be used if full user object is not necessary.

Created users are automatically deleted after the test.


create_users

Scope: function

Depends on: create_user

Shortcut for creating multiple users with single factory call.

from lime_authentication.users import LimeUser

def test_something(create_users):
    users = create_users(
        users=[
            LimeUser(username="user_1", fullname="User 1"),
            LimeUser(username="user_2", fullname="User 2"),
            LimeUser(username="user_3", fullname="User 3"),
        ]
    )

Created users are automatically deleted after the test.


create_group

Scope: function

Depends on: database

Factory fixture that creates a group in the database. Example:

def test_something(create_user, create_group):
    user = create_user(username="my_user", _return_entity=True)
    sub_group_id = create_group(
        name="Best Group",
        user_members=[user],
        _return_entity=False,
    )
    group = create_group(
        name="Even Better Group",
        subgroup_members=[sub_group_id],
        _return_entity=True,
    )

_return_entity flag (which is False by default) controls whether full Group object is returned (True), or just its identifier (False). Returning identifier is faster and should be used if full group object is not necessary.

Created groups are automatically deleted after the test.


add_to_group

Scope: function

Depends on: database

Callable fixture that adds specified users and subgroups to a group.

def test_something(create_user, create_group, add_to_group):
    user = create_user(username="my_user", _return_entity=True)
    sub_group_id = create_group(
        name="Best Group",
        _return_entity=False,
    )
    add_to_group(group=sub_group_id, user_members=[user])
    group = create_group(
        name="Even Better Group",
        _return_entity=True,
    )
    add_to_group(group=group, subgroup_members=[sub_group_id])

It can be used when there is need to add a user / subgroup to the group after it is created. Good example is Administrators or Users groups that are prepopulated in database.

Users and subgroups are removed from groups automatically after the test.


webserver_service_locator

Returns DependencyContainer instance that allows to register webserver dependencies that are used and injected.

from lime_authentication.authentication import AuthenticationMethod

class FakeAuthenticationMethod(AuthenticationMethod):
    ...


def test_something(webserver_service_locator):
    webserver_service_locator.register(
        service=AuthenticationMethod,
        factory=FakeAuthenticationMethod,
    )

web_client

Scope: function

Depends on: lime_databases, lime_app, webserver_service_locator

Requires: admin user with password admin in the database.

Returns FlaskClient that is authenticated as admin user and there is no need to provide API key in the request.

def test_something(web_client):
    response = web_client.get(...)

web_client_non_admin

Scope: function

Depends on: lime_databases, lime_app, webserver_service_locator

Requires: standard user with password standard in the database.

Same as web_client, but authenticated user is standard.


api_client

Scope: function

Depends on: lime_databases, lime_app, webserver_service_locator

Returns FlaskClient that is not authenticated. All requests must contain API key to succeed.

def test_something(api_client):
    response = api_client.get(..., headers={"x-api-key": ...})

prefix_url

Scope: function

Depends on: database

Factory fixture that takes the url on input and prefixes it with database name.

def test_something(web_client, prefix_url):
    response = web_client.get("lime_db/api/v1/users")
    # or
    response = web_client.get(prefix_url("/api/v1/users"))

save_lime_objects

Scope: function

Depends on: lime_app

Callable fixture that simplifies the process of saving lime objects.

def test_something(lime_app, save_lime_objects):
    coworker, company = save_lime_objects(
        lime_app.limetypes.coworker(...),
        lime_app.limetypes.company(...),
    )

Objects returned from save_lime_objects are saved to database. All relation management (attach / detach) must be done before passing objects to callable.

All saved lime objects are automatically removed after the test.


create_file

Scope: function

Depends on: lime_app, database

Factory fixture that creates the file. It uses FileStorage for provided LimeApplication. By default, lime_app fixture output is used.

from lime_application import LimeApplication

def test_something(create_file):
    file = create_file(name="my_file.txt", locked=True)

    file_2 = create_file(lime_application=LimeApplication(...), name="other_file.jpg")

All created files are automatically deleted after the test.


update_service_config

Scope: session

Callable fixture that updates the configuration for the entire service (e.g. webserver). This fixture should be used instead of calling lime_config.load_config(...) which is deprecated.

def test_something(update_service_config):
    update_service_config(
        {
            "globals": {...},
            "features": {...},
            "whatever": {...},
        }
    )

set_app_config

Scope: function

Callable fixture that sets the configuration for the application. This fixture should be used instead of mocking lime_config.get_app_config or creating application_config.yaml only for tests.

def test_something(set_app_config, lime_app):
    set_app_config(
        app_id=lime_app.identifier,
        config={
            "config": {...},
            "secrets": {...},
        }
    )

change_setting

Scope: function

Depends on: database

Callable fixture that changes the given setting in setting table.

def test_something(change_setting):
    change_setting(name="sys_language", value="en_us")

All settings are automatically removed after the test.


Extending fixtures

Fixtures can be extended and still used in combination with rest of them. Some of mentioned fixtures depend on other fixtures, or require some specific state, so as long as these are fulfilled, everything is fine.

To extend the fixture, one must define the fixture with the same name. Such fixture must take on input the fixture being extended.

import pytest


@pytest.fixture(scope="function")
def lime_app(lime_app, save_lime_objects):
    save_lime_objects(lime_app.limetypes.coworker(username=lime_app.user_id))
    return lime_app

Above extension adds coworker for the application. Another example is extending lime types.

from typing import List

import pytest

from lime_type import LimeType, create_limetypes_from_dsl



@pytest.fixture(scope="session")
def lime_types(lime_types) -> List[LimeType]:
    custom_dsl = ...
    custom_limetypes = create_limetypes_from_dsl(custom_dsl)
    return lime_types + custom_limetypes

Note: scopes of the fixture extensions and their output types MUST match with originals.


Replacing fixtures

As per standard pytest API, any fixture can be replaced if other fixture with the same name is defined. As long as scope and output type of replacement matches the original fixture, it can be used with the rest of fixtures described earlier.

from typing import List

import pytest

from lime_type import LimeType, create_limetypes_from_dsl


@pytest.fixture(scope="session")
def lime_types() -> List[LimeType]:
    custom_dsl = ...
    custom_limetypes = create_limetypes_from_dsl(custom_dsl)
    return custom_limetypes

Note: scopes of the fixture replacements and their output types MUST match with originals.


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(web_client, lime_app):
    Company = lime_app.limetypes.company
    Deal = lime_app.limetypes.deal

    uow = lime_app.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 = web_client.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}