Reputation: 2202
I was curious how someone would approach unit testing the following pseudo coded function or even refactor to make it easier to test the different pieces.
To begin, we have a large code base that, at a high level, is broken down into the following projects:
Orchestrations -> Services -> Repositories -> Database
-> Behaviors
The current example I'm working with is at the orchestration level there is a function as follows:
FUNCTION Process (Options)
IF Options.Option1 THEN
IF Service1.HasAnyItems THEN
Service1.DoSomethingWithThoseItems
FI
FI
IF Options.Option2 THEN
IF Service2.HasAnyItems THEN
Service2.DoSomethingWithThoseItems
FI
FI
IF Options.Option1 OR Options.Option2 THEN
Orchestration2.DoSomething
FI
END FUNCTION
I immediately see 4 different test scenarios that will produce a different output:
Currently the function doesn't return anything because the services and orchestration that are called to a variety of things (that are tested separately). To add further challenges, the result of the orchestration call can produce different side effects based on settings that it will internally fetch.
Previously, I have accomplished testing a function like this by mocking out the services and orchestrations and asserting the function was "called". However, I'm not a big fan of this as the mocks are tedious and the tests are very fragile because internal function changes will easily break the tests.
Upvotes: 1
Views: 567
Reputation: 8500
I get your concerns. Verifying internal behaviour instead of inputs vs outputs couples your tests to implementation details and makes them brittle when you do refactorings. Fowler coined this testing style mockist testing in his article Mocks aren't Stubs where he explains and compares mockist and classical testing in detail.
Which testing style is more suitable depends on the programming language used, the system architecture and personal preference.
I'm more of a classical testing guy as well, although I sometimes also rely heavily on mocking if it makes for simpler tests.
That being said, a solution to your problem might be to inverse the control between Process()
and its clients: instead of delegating work to services directly, have it collect the tasks that need to be done and return it. This way you can do regular asserts on the return value of Process
.
In pseudo code:
FUNCTION AssembleProcessingActions (Options) : List OF Action
actions := NEW List OF Action
IF Options.Option1 THEN
actions.Add(Service1.DoSomethingWithItems)
FI
IF Options.Option2 THEN
actions.Add(Service2.DoSomethingWithItems)
FI
IF Options.Option1 OR Options.Option2 THEN
actions.Add(Orchestration2.DoSomething)
FI
RETURN actions
END FUNCTION
Note that I removed the checks to HasAnyItems
. I think they belong inside the DoSomethingWithItems()
methods.
Generally speaking, if your system design is functional rather than object oriented it will lend itself more easily towards classical testing.
This of course does not mean that you cannot have methods on objects anymore and everything should be a static function inside a static utility class. AssembleProcessingActions()
could and should be a method of the type Options
. The point is that it should not change the state of the Options
instance or its dependencies.
Upvotes: 1
Reputation: 13073
Dependency injection and mocking are the fundamental techniques to get ready with unit tests.
If you're not using mocks, you are looking at integration tests and not unit tests. They are basically written the same way (with whatever testing framework you prefer), but they don't work by checking what a single function does.
Instead, your test should call an entry point somewhere in your system (could be something handling a web request, the part that reacts to some button click of a UI, whatever), i.e. the point between a user interaction or a similar trigger and the work that needs to happen.
Suppose your Process (options)
function is indeed such entry point,you now have four scenarios to test (the four possible combinations of Option 1 and Option 2). Thus, you call Process (options)
and check what each of your services and orchestration has done by checking whatever you need to check (filesystem, database, events, ...). There is no other way if you do not want to mock your services.
The mocks are tedious
Maybe so, but everything in the world is tedious at times. Who said programming was fun and challenging all the time? The good news is, you can do this once and never think about it anymore, at least if you write proper test fixtures. If you still need to shuffle dependencies around a lot, you're not designing your system correctly.
Internal function changes will easily break the tests
That's one of the things that testing is for! It makes you double check that you are doing things that makes sense, they help you catch logical mistakes. Besides, the "breakability" of a test determines whether it's written well or not. If it breaks while your application logically does the same thing, it's not a good test. On the other hand, if output changes and your test doesn't break, your not doing it right either.
You may want to pick up a book on unit testing for whatever language you're using.
Upvotes: 3