0

Consider unit tests for two (or more) consequent layers in web-application, backend, eg views (these are concerned with parsing form parameters, rendering the response) and actions (application logic). In the view, calls to actions are mocked. In the actions calls to underlying level are mocked. Tests are fast, but are less useful than they can be, because the connection between layers needs syncing and integration tests are slow and rarer (in theory). Syncing means that mock and mocked functions/methods are in sync. Otherwise we test a mock, which may easily loose connection to reality if not updated, and thus the power of unit tests is diminished.

The question is, has anyone already come with an idea how to keep upper layer mocks with lower level "interfaces" with the goal of making test more maintainable while still being fast? Less stable interfaces are of interest of course.

I always preferred functional, more integration-like tests because they tend to catch more regressions and bugs than mocked separate unit tests. In some cases, application logic is just a simple transaction script with N actions, so mocking does not provide any benefit to testing (unit test too closely follows implementation). However, those are slow, so I am trying to come with some mechanism, and maybe inventing the wheel.

Especially, in the higher level dynamic languages, like Python.

Some example.

function my_script():
    callA()
    callB()
    callC()

Tests will look like (pseudocode):

monkeypatch(callA, dummyA)
monkeypatch(callB, dummyB)
monkeypatch(callC, dummyC)

my_script()

assert dummyB.called_after(dummyA)
assert dummyC.called_after(dummyB)
  • this will not catch callD() should that be added to my_script or side-effects inside those functions or even changes in their signatures (lest say, callB changed to require a parameter). The test will still pass. In this case unit test is useless.
  • 2
    I'm not sure I fully understand. "Tests are fast, but are less useful than they can be" How are they not useful? What are you missing from them that you think a unit test should contain but they currently don't? "because the connection between layers needs syncing" The connection between layers shouldn't be part of a unit test. Also what do you mean by "needs syncing"? – Flater Oct 22 '18 at 6:59
  • Testing with mocks does not have enough benefit as a check but the same (or even greater) work. In case of trivial logic ("transaction script"), there is almost nothing to test if all script "commands" are mocks. Do you agree? – Roman Susi Oct 22 '18 at 18:54
  • Needs syncing explained in the question, thanks. In short, when replacing some object with test double we need to pay attention the test double still resembles the object in the lower level. They need to be in sync. And my question really is, how to automate this without making integration tests? Manually it can be done of course. – Roman Susi Oct 22 '18 at 18:57
  • I also don’t understand. Unit tests focus on making sure the inputs and outputs of a unit work. They shouldn’t care how that happens. Yes, that sometimes means you’re just testing that a few things are called in order. Nothing needs synched here because that is what you’re testing. Other unit tests make sure the other code does what it should. – Telastyn Oct 22 '18 at 18:59
  • 1
    Then go test things that are more likely to fail. – Telastyn Oct 22 '18 at 19:15
3

I really like to use combination of fakes and tests that work against both real and fake implementation. This is useful in case of faking database or other APIs.

This is how testing against DB api would look like: enter image description here

In this scenario some business logic needs some database. What it needs of database is in Database Api. The business logic tests test the business logic alongside fake in-memory database, possibly just lists of structures. This allows the business logic tests to be fast and isolated, even when there is many of them. Second set is Database Api tests. This set, runs same tests twice, once against the fake implementaion and second against real production implementation. This ensures that behavior of the two implementations is the same. Of course the real implementation will persist the data properly, while in-memory will be just local to single use-case.

Big advantage of this design is that business logic tests will run as fast unit tests, so there can be many of them and they can do complex logic without worrying about using slow, real implementation. While the Databse Api Tests can just test specific queries and not worry about the business logic.

  • This is an interesting partial case for domain/persistence layers. It surely accelerates some (many?) tests when in-memory and real database behave the same. If technique like this can be generalized to upper layers - would be perfect answer. – Roman Susi Oct 22 '18 at 19:13
  • 1
    Well it is (IMO) the perfect answer, as it shows a very clean pattern that’s usable for any situation where mocking is needed, at any layer. Keep in mind that mocking isn’t all about speed, it’s also about isolation of failure. You don’t want your whole suite to fail because you broke something in your database class. Instead, only the database unit tests should fail. – Steve Chamaillard Oct 22 '18 at 21:15
  • 1
    So, the answer is To have unit-tests backed with test double layer, which in it's own turn tested against same tests as the real layer? That is, basically having a mocked counterpart for all (or most important) interfaces? I think, this is acceptable answer then. – Roman Susi Oct 23 '18 at 3:28

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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