Reputation: 1723
Say you have 3 functions, functionA, functionB, and functionC
functionC relies on functionA and functionB
functionA(a) {
return a
}
functionB(b) {
return b
}
functionC(a, b){
return functionA(a) + functionB(b);
}
Now this is obviously a super simplified example.. but what's the correct way to test functionC? If I'm already testing functionA and functionB and theyre passing wouldn't testing functionC wind up being more than a unit test since it would be relying on functionA and functionB returns..
Upvotes: 4
Views: 1599
Reputation: 131526
The GhostCat answer is simple, fine and focus on the essential.
I will detail about some other points to consider, particularly the refactoring question.
Unit tests focus on API
The classes API (public functions) have to be unit tested.
If these 3 functions are public, each one has to be tested.
Besides, unit tests don't focus on implementation but expected behavior.
Today the composite function add individual function results, tomorrow it could substract them or anything else.
Testing the C()
composite function doesn't mean testing again all scenarios of A()
and B()
, it means testing the expected behavior for C()
.
In some cases, unit testing a composite function in integration with individual functions doesn't generate many duplication concerning individual functions.
In other cases, it does. I will present it in the next point.
Example where testing the C()
composite function may cause a duplication concern in the tests.
Suppose that the A()
function accepts two integers :
function A(int a, int b){ ...}
It has the following constraints about the input parameters :
If one of these is not respected, an exception is thrown.
In the A()
unit test, we will test each one of these scenarios. Each one probably in a distinct test case :
@Test
void A_throws_exception_when_one_of_params_is_not_superior_or_equal_to_0(){
...
}
@Test(expected = InvalidParamException.class);
void A_throws_exception_when_one_of_params_is_not_inferior_to_100(){
...
}
@Test(expected = InvalidParamException.class);
void A_throws_exception_when_params_sum_is_not_inferior_to_100(){
...
}
Aside the error cases, we could also multiple nominal scenarios for the A()
function according to the passed parameters.
Suppose that the B()
function has also multiple nominal and error scenarios.
So what about the unit test of C()
that aggregates them ?
You should of course not re-test each one of these cases. It is a lot of duplication and besides it will have more combination by crossing cases of the two functions.
The next point presents how to prevent duplication.
Possible refactoring to improve the design and reduce the duplication in the unit tests of the composite function
As you write composite functions, the first thing that you should wonder is whether the composite functions should not be located in a specific component.
composite component -> unitary component(s)
Decoupling them may improve the overall design and give more specific responsibilities to components.
In addition, it provides also a natural way to reduce duplication in the unit tests of the composite component.
Indeed, you can, if required, stub/mock unitary component behaviors and don't need to create detailed fixtures for them.
Composite component unit tests can so focus on the composite component behavior.
So in our previous example, instead of testing all cases of A()
and B()
as we unit-testing the C()
function, we could stub or mock A()
and B()
in order that they behavior as expected for the C()
scenarios.
For example for the C()
test scenario with error cases related to A()
and B()
, we don't need to repeat each A()
or B()
scenario cases :
@Test(expected = InvalidParamException.class);
void C_throws_exception_when_a_param_is_invalid(){
when(A(any,any)).thenThrow(new InvalidParamException());
C();
}
@Test(expected = InvalidParamException.class);
void C_throws_exception_when_b_param_is_invalid(){
when(B(any,any)).thenThrow(new InvalidParamException());
C();
}
Upvotes: 1
Reputation: 3593
In my opinion your tests should not know that functionC is using functionA and functionB. Normally you create automatic tests, to support change (code maintenance). What if you change the implementation of C? All the tests of functionC become invalid as well, that is unnecessary and dangerous, because that means, the refactorer must understand all the tests as well. Even if he/she is convinced, that he/she is not changing the contract. If you have a great testcoverage, why should he/she do that? So the public contract of functionC is to be tested in full!
There is a further danger, if the tests know too much about the inner workings of the sut(functionC) they tend to reimplement the code inside. So the same (probably faulty) code that does the implementation, checks if the implementation is correct. Just an example: How would you implement the (whitebox) test of functionC. Mock functionA and functionB and look if the sum of the mocked results is produced. That is just good for the test-coverage (kpi??) but can be also quite misleading.
But what about the high extra effort of testing the functionality of functionA and functionB twice. If that is so, then probably reuse of the testing code is easily possible, if the reuse is not possible, I think that confirms my earlier statements the more.
Upvotes: 4
Reputation: 140613
For the first two functions you would focus on their public contract - you would write as many tests as required to ensure all results for different cases are as expected.
But for the third function it might be sufficient to understand that the function should be invoking the other two functions. So you probably need less tests. You don't need to test again all cases required for testing A and B. You only want to verify the expected "plumbing" that C is responsible for.
Upvotes: 5