1

Within our codebase we have integration tests like these:

func TestActivateSuspendedFoo() {
    // setup all dependencies
    baz := jsonToStruct(
        "baz": {
            "a": "someInfo",
            "status": "active",
            "start_date": "01/01/2020",
        }
    )
    SendCommand("create_baz", baz)
    ExpectResponse("baz_created")

    bar := jsonToStruct(
        "bar": {
            "baz": &baz,
            "start_date": "01/01/2020",
            "end_date": "02/01/2020",
        }
    )
    SendCommand("create_bar", bar)
    ExpectResponse("bar_created")

    foo := jsonToStruct(
        "foo": {
            "bar": &bar,
            "status": "suspended",
        }
    )
    SendCommand("create_foo", foo)
    ExpectResponse("foo_created")
    
    // finally setup scenario data
    SendCommand("activate_foo", foo)

    foo.status = "active"

    ExpectResponse("foo_activated", foo)
}

Setting these up is painful because:

  1. The developer in charge of "activate_foo" must know that foo depends on bar which depends on baz, and how they are set up (specially hard for new hires).
  2. When testing "create_foo", it has the exact same previous set up, same for "create_bar" up to its point (lots of duplication).
  3. Every SendCommand is hitting the database, which can fail due to some instability during its run and pass without changes on a second run (unstable).

We are currently looking for means to ease the data setup, reduce duplication and make tests more stable, what are the alternatives to address the situation?

Some things to consider:

  1. There are some utility classes that set up some defaults for each of these structures, but they are brittle since the author could have a different understanding of a default case is for a given structure (it isn't wrong for baz to have a different status other than active, or a start_date different from the input one, but that could have downstream effects for other scenarios).
  2. It's also difficult to simply write the dependencies to the database without using their dedicated APIs since internally these APIs could also trigger other validations/updates that are expected for its business domain.
  3. This is a Golang project and we don't use an ORM (no good candidates ATM) and it would be too expensive to make this change even for mid-term (big project), we use an internal abstraction for the DB's APIs (MongoDB).
  4. Semi-related to How to setup data for service tests
  • 1
    The approach to testing a certain part of a system depends entirely on the kind of test you're trying to produce. You have used three types of tests as your tags for this question: unit, integration and acceptance. Which exact type are you after? Please, clarify it in the question. – Andy Jun 17 at 7:49
  • @Andy I've tagged all these types because I'm open to suggestions on how to break these down: does it make more sense to break these into unit tests depending on mocks? Maybe change the styling to look closer to an acceptance testing? These are currently considered integration tests on our code base. I'll try to make this clearer on the question, thanks for the suggestion. – Filipe Gorges Reuwsaat Jun 17 at 14:10
1
+100

Mocks

Mock out the Bar object. That way you need only understand what Foo is expecting out of Bar, which is reasonable for a Unit Test.

Builders

If you need real data, it might pay to architect a Builder.

The Builder Object would have an interface (usually fluent, but do you) that would allow a developer to succinctly describe a number of common forms of data.

The builder would then be responsible for defaulting and filling in all of the unspecified details, such as making baz and bar objects correctly. This is usually done by a call to a method like builder.build() which returns the built object.

As a bonus the builder could see use in the application itself as part of the UI, or a data generator. It may even have safe-guards involved to warn errant programmers/users that something just won't work. Like Options A and B are exclusive, or that Value C must be provided.

| improve this answer | |
  • The issue with mocks is that Bar also depends on a Baz object which would need to be mocked for Bar before we can use Bar for Foo, and this is assuming that we only have 3 levels of dependencies. I like the approach of mocking for the most part but how would we evade the issue of setups? – Filipe Gorges Reuwsaat Jun 17 at 15:50
  • I like your suggestion on Builders, I'll try that on a POC and come back for an update with the findings, thanks! – Filipe Gorges Reuwsaat Jun 17 at 15:50
  • Why would the mock rely on baz? Its an implementation detail of Bar, unless Bar is exposing it and forcing you to use trainwreck notation: a.b.c.d.e. In which case you should refactor that. It will make your life simpler not just in the test code, but also in the main program. – Kain0_0 Jun 17 at 23:26
1

I don't have enough reputation to comment, but I'll follow up on what @Kain0_0 said and your comments to his answer.

If you mock your Bar dependency, you don't need to mock out its dependency of Baz too - the mock allows you to ignore that Bar even depends on Baz! With concrete objects, this may seem weird, but if you extract interfaces for your Baz and Bar object (let's say, IBar and IBaz), you'll realize that Foo only dependends on IBar, which mean you can inject him with anything that respect the contract of IBar, even a mock which does not depend on Bar or IBar.

This will make testing a whole lot easier than implementing builders for every object, and having to mock the whole dependency tree of your objects.

| improve this answer | |
  • 1
    Good explanation. Just be cautious, sometimes systems are data driven in which case it is actually simpler to correctly build an object. Consider a physics simulation using vectors. – Kain0_0 Jun 25 at 23:37
  • 1
    Definitely. I was explaining a more traditional OOP system, but for data-driven programs a more "concrete" approach to testing is the way to go. – Kawemi Jun 27 at 15:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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