Remix.run Logo
troupo 2 days ago

What he wrote is basically "don't repeat in test X what you already tested in test X-1".

It's not as much composition as compounding, and can work quite well.

Let's say something takes 20 steps, and you want to test all 20.

Instead of this:

    test 1:
       do step 1, assert step 1

    test 2:
       do step 1, assert step 1
       do step 2, assert step 2

    test 3:
       do step 1, assert step 1
       do step 2, assert step 2
       do step 3, assert step 3

    ...
you do this:

    test 1:
       do step 1, assert step 1

    test 2:
       do step 1
       do step 2, assert step 2

    test 3:
       do step 1
       do step 2
       do step 3, assert step 3

    ...
This works well in certain situations, as it skips diplicated redundant testing. Requires some discipline so that tests don't drift away from each other.

As most things, it depends on what those steps are. Perhaps you only need that one final (integration) test instead of 20 intermediate unit ones.

RHSeeger 2 days ago | parent | next [-]

I wonder if it would work do design something that was able to say

    test 1: 
       do step 1, assert step 1

    test 2:
       requires: test 1
       do step 2, assert step 2

    test 3:
       requires: test 2
       do step 3, assert step 3

    test 4:
       requires: test 2
       do step 3, assert step 3
If the assertion of each test doesn't change any state, that might make things easier to read. Though, given that I haven't spent much time pondering it, I expect it could have it's own problems.

But it could also do things like skip test 3 if tests 2 or 1 failed - because it knows about the relationship.

drdexebtjl 2 days ago | parent | next [-]

The hard parts to design about this, imo, are:

- How to reconcile this with tests that execute many times with varying input data. You’d need some way to express requirements with specific inputs or shared inputs.

- Passing state between test dependencies.

- When, if ever, it’s fine to share step results between tests. If tests B and C require A, can you run A just once? Not always, but you should be able to when it’s safe.

I don’t think I’ve ever used a test framework that gets these things right.

RHSeeger 2 days ago | parent [-]

> Passing state between test dependencies

Actually, I wasn't even thinking about passing state. I was thinking about shared setup steps. I'm perfectly happy for the same steps to run for each test - as long as each test doesn't need to list the steps (when they're setup steps not directly related to the thing being tested)

drdexebtjl 2 days ago | parent [-]

For example, suppose you want to write a test for the shipOrder(orderId) function, and you want it to depend on the test for the placeOrder(shoppingCart) -> OrderId function. Even if you are fine calling placeOrder twice, once for the placeOrder test and once for the placeOrderAndShipOrder test, you still need the placeOrder test to provide an order ID to the second test, and not just a confirmation that it completed successfully.

avensec 2 days ago | parent | prev | next [-]

The pattern can work, but the domain matters, the test type matters (unit, integration, ui) and the trade-off associated matter.

e.g. If I am running a long-running UI-test scenario, I absolutely don't want test-5 to walk through 80% of the UI that was already exercised in tests 1-4. I am creating test coupling, but I'm saving cost/time by doing so.

But, you'll also hear why not to do this, because it creates test coupling / breaks atomic tests, which is generally seen as bad.

If that is a local integration test and those early steps run is millis? Then maybe we keep things uncoupled to allow the system to exercise the pathways without explicit expectations.

RHSeeger 2 days ago | parent [-]

The question was less about speed and more about not having the same code duplicated over and over across tests. Which is how I read the article talking about it. Allowing one test to "depend" on another makes it clear they use the same setup (presumably with the second test going "a bit further", but not necessarily).

I wouldn't have a problem with something like

    test-1:
        setup:
            do-the-thing
        verification
            assert-the-thing-happened
    test-2
        setup:
            depends-on: test-1 // tells it to run test-1's setup
            do-the-next-thing
        verification
            assert-the-next-thing-happened
The format is awful, but the idea is that most tests are of the form

    GIVEN
       Some initial setup
    WHEN
       I run command
    THEN
       The result of that command is what is expected
And, in that context, the GIVEN frequently contains noise not directly related to understanding what is being tested.

I actually use the GIVEN/WHEN/THEN keywords in my tests, to make them easier to read

brabel 2 days ago | parent | prev | next [-]

Yes that is the way to do it, in Spock that’s what @Stepwise does.

mamcx 2 days ago | parent | prev | next [-]

This make too much sense!

In special when testing against a DB.

troupo 2 days ago | parent | prev | next [-]

Yup. I'd love to have dependency declarations for tests like this.

jaggederest 2 days ago | parent [-]

That's... A thing in many frameworks. If it's not in yours, you could add it.

Forgive an old man some ruby:

    it 'relies on mobile setup defined elsewhere', :mobile => true do
      # test that relies on mobile setup here
    end
latencyharbor 2 days ago | parent | prev [-]

[flagged]

jayd16 2 days ago | parent | prev | next [-]

IMO it seems kind of terrible.

You're not going to remember what tests "test 3" relies on. They aren't actually linear progressions 123. They will be "test this", "test that". If 3 fails you're going to want to immediately go and add all those asserts back to help you debug your assumptions.

The tests _will_ drift and that should be fine. Implicitly depending on other tests doesn't really get me anything.

seanwilson 2 days ago | parent | prev | next [-]

What's the problem of only keeping test 3 if it depends on test 2 and 1 passing anyway?

The article mentions not to do this because "Deleting test1 loses us another property from the Test Desiderata—tests should be specific. That’s the property of tests where, when one fails, you know exactly where the problem is." but you'll know what line it failed on. And some test runners let you break a test into steps, where groups of lines are given a description.

Or put each step + assert in a helper function (e.g. `doStep1AndAssert()`), and each test only calls these helper functions?

Nothing is perfect, but copy/pasting chunks between tests like this isn't great when you want to refactor and it's repetitive to read.

troupo 2 days ago | parent [-]

> What's the problem of only keeping test 3 if it depends on test 2 and 1 passing anyway?

That depends.

Sometimes test 1 tests a combination of ways (e.g., property testing, or just going through a bunch of various inputs), and only a few of those are needed for test 2.

Sometimes you don't want your test 2 to be more complicated than it already is. Or the same things are needed checked in other tests. So you extract them into test 1.

And sometimes (and in some of code bases most of the time) test 1 is redundant and unnecessary. That's why I always advocate investing in integration tests (test 20) and skip all the intermediate tests.

skydhash 2 days ago | parent [-]

> That's why I always advocate investing in integration tests (test 20) and skip all the intermediate tests.

My approach is to have acceptance tests documented for any feature. Like how it would be from the user point of view to actually use the software. Then do Integration tests for each part of that workflows. That's usually the most ROI you will get for testing. Then I invest into unit tests for particular elements that are very important. I start from the middle of the pyramid because an actual e2e is expensive to setup (easy to maintain afterwards) and having lots of unit tests (easy to setup) is expensive to maintain.

mrkeen 2 days ago | parent | prev [-]

I think I disagree with Kent, but your explanation is clearer, so I'll object here.

There's nothing wrong with hitting the same assertion multiple times, even if it doesn't sit nicely in your gut.

From a purely philosophical point of view: If I have testFoo(), testBar(), and testFooAndBar(), and my Foo is plain wrong, then both testFoo() and testFooAndBar() must fail. Anything less is misleading/dishonest.

From a practical side: Changes happen. Someone will remove testBar(), and then you're down to 0 assertions on Bar, even though you have a test claiming to testFooAndBar(). It's not even a crazy hypothetical. Someone with a different test philosophy will think (to quote TFA) "They are redundant! Something must be wrong." and delete testBar() because obviously testFooAndBar() already covers it.

Anyway, we all know how to deal with repetition. That's what programming is!

  testFoo()
  _ = validateFoo(foo())

  testBar()
  _ = validateBar(bar())

  testFooAndBar()
  foo = validateFoo(foo())
  bar = validateBar(bar())
  _   = validateFooAndBar(fooAndBar(foo, bar))
hungryhobbit 2 days ago | parent [-]

If you have people on your team deleting valid tests because of "philosophy", I think you have much bigger problems to solve than anything Kent Beck can help with.

mrkeen 2 days ago | parent [-]

To be clear, the philosophy I quoted was directly from Kent Beck in TFA, i.e. this is Kent Beck's "help".

I say leave both tests as is.

Kent Beck says:

  From a purely aesthetic standpoint (& don’t discount aesthetics), leaving both tests as is offends my sensibilities. They are redundant! Something must be wrong.
It's not just philosophy, it's aesthetics apparently!