-1

I am writing the embedded firmware for an effect pedal. The pedal's ui consists of a few knobs a few buttons and a few leds and it consists of various control modes each corresponding to a seperate effect. So each mode changes the meaning of the knobs, the buttons and led indications.

This is implemented as a sum type of "presenters". Each presenter is injected with the corresponding effect model and the buttons + leds drivers. Each presenter has it's own set of shortcuts and button controls. Each presenter owns the corresponding view and event emitter.

I am trying to figure out how to write integration tests for these presenters. What I currently do is pinpoint the high - level button events required for each presenter. Then define a state machine using the high-level events and define the appropriate model/view interactions for each transition. Then I use model-based testing to test that the implemented state machine properly calls everything.

I independently test the high-level event emitter which produces the events for the state machine.

I find this helps a lot with properly testing the interactions since writing the model-based tests for the state machine using high-level events is much more expressive than using the buttons driver directly.

So as a summary each presenter basically just delegates to the event emitter and passes the events from the event emitter to the state machine (called controller in the code) which controls the model and view.

A final presenter may look like this:

    void delay_presenter::update()
    {
        button_event_emitter_.poll();

        bool exit_event = button_event_emitter_.test_and_clear_exit_event();
        bool tap_event = button_event_emitter_.test_and_clear_tap_button_event();
        std::optional<button_event> enable_event = button_event_emitter_.test_and_clear_enable_button_event();

        bool alternative_modified = alternative_modifed_event_emitter_.poll();

        if (exit_event)
            controller_.handle_exit_event();

        if (tap_event)
            controller_.handle_tap_button_event();

        if (enable_event)
            controller_.handle_enable_button_event(*enable_event);

        if (alternative_modified)
            controller_.handle_alternative_modified_event();

        controller_.handle_switch_position(gpios_.switch_position());

        model_.set_inputs(inputs_);

        view_.update();
    }

I am in conflict in how to properly test these presenters without duplicating the test suit of the underlying controller objects but in a more verbose way. It seems that if I don't want to couple the presenter's tests to the underlying implementation, I will have to basically merge the event emitters' and controller's test suits together which makes the model-based testing much more verbose and complex. In fact it's often the case that I would have to use the event emitter object itself in the test suit too.

I think this leads to a more general problem on how one should test a component that basically just wires together other subcomponents that are heavily tested themselves and were built in a bottom-up approach.

One solution is to just make sure that the component wires the subcomponents together properly. In that case I would have to mock the controller and event emitters and make sure calls are delegate as appropriate. This would be the Mockist-approach to this problem. I can't think of any way of doing it without basically duplicating the actual code and making a very fragile test that will break with any future refactoring of the presenter. Maybe this is a good thing in this case since any change to the emitter + controller architecture would probably need a redesign of the test themselves anyway.

Another is to perhaps just write a few sanity check tests with a limited number of user scenarios. This won't be a complete test as it is with the model-based approach used on the controller state-machine object. I can't see this helping much in the case of a larger refactoring.

Going further how should one implement the final end-to-end test that actually checks all presenters? It seems one would have to either just check a very limited subset of use cases or do intrusive mock-based tests checking presenter destruction and creation.

Any thoughts?

1 Answer 1

2

You can test this kind of high-level presenters with domain use-case scenarios.

There are many technologies to write such scenarios. The simplest might be a Unit Testing frameworks used with the user in mind, ex:

void test_user_scenario_x() {
    given_a_user_has_loged_in(name: "John");

    when_user_does_xxx();

    then_user_sees_yyy();
}

You can also use even more domain-oriented tools like Cucumber. As you work your way through the implementation and testing, you will find cases that don't really fit these domain-oriented tests, this is when typical unit tests are useful. With a more specific context, it should be easier to choose between a mockist or classic approach.

If you are working with a team and with domain experts (eg. you are not the domain expert) then having a tool like Cucumber offers some extra communication value if you involve the domain experts in the test writing.

Whatever the tool you use, a nice thing with these domain-oriented tests is that they separate the user scenario from the implementation of the steps. This lets you use the same scenario to test the same domain logic on different layers of your system: the domain model, the presenter, or the full UI. For example:

  • you could inherit this test class and use the Template Method pattern to provide bindings to your presenter, the UI, and/or the domain model
  • if you are using tools like Cucumber, you can provide a new set of Step Definitions for every layer. Aslak Hellesøy wrote about this in Polymorphic Step Definitions

This technique removes duplication and also gives you an easy way to select the set of scenarios to run on the different layers. For example:

  • all of them on the domain classes
  • enough on the presenter to have good coverage and catch enough errors
  • just a few through the UI to make sure everything is wired up correctly and works as expected

As Aslak says it

Cucumber power users who understand the benefits of the test pyramid will prefer to run most scenarios against the middle layer (without going through a UI). Then they'll pick a small subset of the same scenarios to run through the UI and run those separately.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.