Skip to content

Running tests

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.

Info

Our standard CI toolchain will fail any build containing any test 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.

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

lime-test ships a large set of ready-made fixtures — see the Fixtures section for an overview, the conventions they follow, and a per-fixture reference.

Mocking

The Lime CRM platform wires services together with a dependency injection container, and that shapes how you should write test doubles. The guiding principle is to keep tests connected to real behavior: prefer real objects, inject test doubles through the container rather than patching, and reach for mocking only at genuine external boundaries.

Prefer real objects over mocks

Don't mock what you can pass in or inject. If a dependency can be provided as a parameter or through a fixture, do that instead of reaching for unittest.mock or monkeypatch.

When you do need a test double, write a fake — a small implementation of the interface — rather than a mock. Reserve mocking for true external boundaries (network calls, third-party services) where no other option exists.

import abc


class MyService(abc.ABC):
    @abc.abstractmethod
    def get_value(self) -> int: ...


class FakeMyService(MyService):
    def __init__(self, value: int):
        self._value = value

    def get_value(self) -> int:
        return self._value


@pytest.fixture
def my_service() -> MyService:
    return FakeMyService(value=42)


def test_uses_service_value(my_service):
    result = compute(my_service)

    assert result == 42

A fake stays honest as the interface evolves — if MyService gains a new abstract method, the fake fails to instantiate until it implements it, whereas a Mock(spec=MyService) would silently keep passing.

Swap injected services through the container

When the code under test resolves a dependency from the DI container, override that registration for the test instead of patching the implementation. Define a lime_test_component_registrations__ fixture that returns the replacement registration — lime-test collects it and registers it in the container for the duration of the test.

from lime_core.dependencies import (
    ComponentRegistration,
    ComponentRegistrationMeta,
)


@pytest.fixture
def lime_test_component_registrations__fake_my_service(
    request,
) -> list[ComponentRegistration]:
    return [
        ComponentRegistration(
            service=MyService,
            factory=lambda: FakeMyService(getattr(request, "param", 42)),
            lifestyle="scoped",
            meta=ComponentRegistrationMeta(label="test"),
        )
    ]

This keeps production wiring untouched: the application still resolves MyService from the container, it just receives the fake. The request.param indirection lets a single fixture serve many tests through indirect parametrization. See lime_test_component_registrations for activation, naming variants, and the difference between replacing a registration and adding a new one.

Warning

meta=ComponentRegistrationMeta(label="test") replaces an existing registration. If the service is not already registered by the service being bootstrapped, the registration is silently dropped — omit meta to register a brand-new service instead.

Spies are fine — replacing behavior is not

Sometimes you need to verify that a dependency was called with the right arguments, or the right number of times. A spy (for example mocker.spy) is fine for this, because the real implementation still runs. What to avoid is replacing behavior with MagicMock, Mock, or patch — that disconnects the test from reality, so nothing production-like actually executes.

# Good — the spy wraps the real method, production code still runs
def test_service_is_queried_once(mocker, my_service):
    spy = mocker.spy(my_service, "get_value")

    compute(my_service)

    spy.assert_called_once()


# Bad — MagicMock replaces behavior, nothing real runs
def test_service_is_queried_once():
    my_service = MagicMock(spec=MyService)

    compute(my_service)

    my_service.get_value.assert_called_once()

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 torn down immediately afterwards.

It's an implementation of Flask's test client.

A simple example

Given the following endpoint in the acme_solution plugin:

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:

import json


def test_calculate_average_for_deals(
    web_client, lime_app, prefix_url, save_lime_objects
):
    company = lime_app.limetypes.company(name="acme")
    deals = [
        lime_app.limetypes.deal(name="deal1", value=42),
        lime_app.limetypes.deal(name="deal2", value=128),
    ]
    for deal in deals:
        company.properties.deal.attach(deal)

    company, *_ = save_lime_objects(company, *deals)

    response = web_client.get(prefix_url(f"acme_solution/dealaverage/{company.id}"))

    assert response.status_code == 200
    assert json.loads(response.data) == {"average": 85.0}

Refactoring tests to use new fixtures

Many codebases still rely on old fixtures, primarily due to their inclusion in lime-project templates. However, these fixtures significantly degrade the performance of testing suites compared to the newer ones. It's recommended that whenever a project with old fixtures is encountered, the test files should be rewritten to use the new ones. Here is a short guide on how to act when working on refactoring the test files.

  • Find the conftest.py file and look if old fixtures are used.
  • Replace the limetypes fixture with code below (if you use something else than custom_dsl.dsl, use what you had before).
@pytest.fixture(scope="session")
def lime_types():
    return lime_type.create_limetypes_from_dsl(custom_dsl.dsl)
  • Remove the limeapp fixture in order to use the framework's lime_app.
  • Remove the database fixture (the framework's fixture of the same name will be used automatically).
  • Replace all occurrences of limeapp with lime_app.
  • Replace all occurrences of webapp to web_client (exclude the changes which may accidentally be done to poetry.lock).
  • Run all tests and make adjustments if anything breaks.

Tips and tricks

Endpoint tests gives an error when no coworker is set for app

This is caused because in the DSL, the coworker does not have a label user, which makes it impossible to read the attribute lime_app.coworker. The DSL file should be modified with the proper user label set.

If the problem still exists, it may be because the limepkg-dsl-extract overrides the label property. The fix for that is currently in progress and the documentation will be updated after the proper release.

Adding a specific type of user to app.user

If you need a lime_app with specific app.user for some reason, maybe special functionality for a portaluser you can create a separate lime_app instance for that matter. You can achieve that by using lime_test.app_create_app and passing the created user as active_user argument. Example:

import lime_test

@pytest.fixture
def lime_app_portal_user(database, lime_types, create_user, monkeypatch):
      user: LimeUser = create_user(
          username="apiuser_portal@lime.tech", 
          password="password",
          full_name="Portal User", 
          _return_entity=True
      )

     app = lime_test.app.create_app(
         monkeypatch=monkeypatch,
         database=database,
         limetypes=lime_types,
         active_user=user,
     )
     return app

The tests take a long time to run

This may happen due to a bug in lime-crm tests. This bug is solved now, upgrade to minimum 2.936.2 CRM version to solve it.