Testing couchdb

It is quite hard to test couchdb currently. The main difficulties are related to complex setup functions and weak isolation of test cases. There are some functions in test_util module which simplify setup a little bit but not by much. The purpose of this proposal is to define some requirements for testing infrastructure and proposing a solution which satisfy most of these requirements.

Why is it so hard to write certain kinds of tests?

What might make things easier?

Proposal

couch_test (cdt) design

couch_test/
  +-- include/
    +-- intercept.hrl
    +-- cdt.hrl
  +-- intercepts/
  +-- setups/ - Would contain setup/teardown code for reuse
  +-- src
    +-- intercept.erl
    +-- cdt.erl
    +-- combinatorics.erl
  +-- bin/
    +-- cdt
  +-- rebar.config
  +-- etc/
    +-- cdt.conf.example
    +-- local.conf.example
    +-- test_cluster.conf.example

Just to illustrate the idea here is the list of commands for cdt (eventually).

cdt --help

setup modules

-module(cdt_chttpd_setup).

-export([chttpd/4]).

chttpd(A, B, C, D) ->
    {setup(A, B, C, D), teardown()}.

setup(A, B, C, D) ->
    fun(Ctx) ->
        %% use A and B to setup chttpd
        %% store C and D in context for latter use in tests
        NewCtx = update_ctx(Ctx, C, D),
        {?MODULE, NewCtx}
    end.

teardown() ->
    fun(Ctx) ->
       %% Clean up the things using Ctx to find out what to do
       {?MODULE, Ctx}
    end.

Setups could be composed into a chain

-module(my_unit_test).

-import_lib("cdt.hrl").

%% Imports are optional
-import([cdt_chttpd_setup, [chttpd/4]]).
-import([cdt_couch_setup, [couch/3]]).
-import([cdt_cluster_setup, [cluster/1]]).
-import([cdt_fault_setup, [disconnect/2, drop_packets/3]]).

setup(Type) ->
    Chain = [
        couch(Type, 1, 2),
        chttpd(backdoor, foo, bar, baz),
        cluster(3),
        disconnect(1, 2),
        drop_packets(2, 3, 30) %% 30% of packets to drop between db2 and db3 nodes
    ],
    Args = [],
    Opts = [],
    cdt:setup(Chain, Args, Opts).

teardown(_Type, Ctx) ->
    cdt:teardown(Ctx).

Injecting networking problems

Since a crossplatform solution is required it is better to use something erlang based for fault injection. We could extend epmdpxy to simulate latency or connectivity problems. It should be extended in such a way to be able to selectively induce problems between specified nodes (without affecting the communication between test master and test slaves). In this case nodes should be started using:

ERL_EPMD_PORT=43690 erl

Tapping to logger

Don't log or produce noisy output by default. However should be able to control verbosity. It is also possible to split the output.

In some cases should be able to:

The easiest way to achieve both goals is using permanent intercept for couch_log.erl. Another approach could be a special couch_log backend.

Fixtures

Store fixtures in tests/fixtures of the applications we are testing. We also might have some common fixtures in couch_test/fixtures. All fixtures should be templates. couch_test app would have some helpers to find and include the fixtures.

It would be helpful to support following types of fixtures:

Grouping test cases

Use list of lists as a test name to determine if grouping is needed. For example:

apply_options_test_() ->
    Funs = [fun ensure_apply_is_called/2],
    Cases = combinatorics:powerset([pipe, concurrent]),
    cdt:make_cases(
        ["apply options tests", "Apply with options: ~p"],
        fun setup/1, fun teardown/2,
        Cases, Funs).

This would generate following test cases

{
    "apply options tests",
    [
        {
            "Apply with options: []",
            [
                {
                    foreachx, fun setup/1, fun teardown/2,
                    [
                        {[], fun ensure_apply_is_called/2}
                    ]
                }
            ]
        },
        {
            "Apply with options: [pipe]",
            [
                {
                    foreachx, fun setup/1, fun teardown/2,
                    [
                        {[pipe], fun ensure_apply_is_called/2}
                    ]
                }
            ]
        },
        {
            "Apply with options: [concurent]",
            [
                {
                    foreachx, fun setup/1, fun teardown/2,
                    [
                        {[concurent], fun ensure_apply_is_called/2}
                    ]
                }
            ]
        },
        {
            "Apply with options: [pipe, concurrent]",
            [
                {
                    foreachx, fun setup/1, fun teardown/2,
                    [
                        {[pipe, concurrent], fun ensure_apply_is_called/2}
                    ]
                }
            ]
        },
    ]

Tests annotations

In order to distinguish kinds of tests we would need to annotate test cases. We could use one of the following in order of my personal preference (any other ideas?):

  1. Implement parse transform using merl to support annotations:

    -scope([integration, unit, cluster]).
    my_tests_() - >
        ok.
    
  2. Split different kinds of tests into different modules and maybe keep them in different directories

  3. Pass scope to cdt:make_cases.
  4. Introduce naming convention for a test name

  5. Have a module where we add every test case into approporiate scope.