Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid duplicate logic with Mocks

I have the following challenge, and I haven't found a good answer. I am using a Mocking framework (JMock in this case) to allow unit tests to be isolated from database code. I'm mocking the access to the classes that involve the database logic, and seperately testing the database classes using DBUnit.

The problem I'm having is that I'm noticing a pattern where the logic is conceptually duplicated in multiple places. For example I need to detect that a value in the database doesn't exist, so I might return null from a method in that case. So I have a database access class which does the database interaction, and returns null appropriately. Then I have the business logic class which receives null from the mock and then is tested to act appropriately if the value is null.

Now what if in the future that behavior needs to change and returning null is no longer appropriate, say because the state has grown more complicated, so I'll need to return an object that reports the value doesn't exist and some additional fact from the database.

Now, if I change the behavior of the database class to no longer return null in that case, the business logic class would still appear to function, and the bug would only be caught in QA, unless someone remembered the coupling, or properly followed the usages of the method.

I fell like I'm missing something, and there has to be a better way to avoid this conceptual duplication, or at least have it under test so that if it changes, the fact that the change is not propagated fails a unit test.

Any suggestions?

UPDATE:

Let me try to clarify my question. I'm thinking of when code evolves over time, how to ensure that the integration doesn't break between the classes tested via the mock and actual implementation of the classed that the mock represents.

For example, I just had a case where I had a method that was originally created and didn't expect null values, so this was not a test on the real object. Then the user of the class (tested via a mock) was enhanced to pass in a null as a parameter under certain circumstances. On integration that broke, because the real class wasn't tested for null. Now when building these classes at first this is not a big deal, because you are testing both ends as you build, but if the design needs to evolve two months later when you tend to forget about the details, how would you test the interaction between these two sets of objects (the one tested via a mock vs the actual implementation)?

The underlying problem seems to be one of duplication (that is violating the DRY principle), the expectations are really kept in two places, although the relationship is conceptual, there is no actual duplicate code.

[Edit after Aaron Digulla's second edit on his answer]:

Right, that is exactly the kind of thing I am doing (except that there is some further interaction with the DB in a class that is tested via DBUnit and interacts with the database during its tests, but it is the same idea). So now, say we need to modify the database behavior so that the results are different. The test using the mock will continue to pass unless 1) someone remembers or 2) it breaks in integration. So the stored procedure return values (say) of the database are essentially duplicated in the test data of the mock. Now what bothers me about the duplication is that the logic is duplicated, and it is a subtle violation of DRY. It could be that that is just the way it is (there is a reason for integration tests after all), but I was feeling that instead I'm missing something.

[Edit on starting the bounty]

Reading the interact with Aaron gets to the point of the question, but what I'm really looking for is some insight into how to avoid or manage the apparent duplication, so that a change in the behavior of the real class will show up in the unit tests that interact with the mock as something that broke. Obviously that doesn't happen automatically, but there may be a way to design the scenario correctly.

[Edit on awarding the bounty]

Thanks to everyone who spent the time answering the question. The winner taught me something new about how to think about passing the data between the two layers, and got to the answer first.

like image 985
Yishai Avatar asked Mar 13 '09 16:03

Yishai


People also ask

Why or when is it useful to test using mocks?

Only use a mock (or test double) “when testing things that cross the dependency inversion boundaries of the system” (per Bob Martin). If I truly need a test double, I go to the highest level in the class hierarchy diagram above that will get the job done. In other words, don't use a mock if a spy will do.

What are the point of mocks?

Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.


2 Answers

You are fundamentally asking for the impossible. You are asking for your unit tests to predict and notify you when you change the external resource's behaviour. Without writing a test to produce the new behaviour, how can they know?

What you are describing is adding a brand new state that must be tested for - instead of a null result, now there is some object coming out of the database. How could your test suite possibly know what the intended behaviour of the object under test should be for some new, random object? You need to write a new test.

The mock is not "misbehaving", as you commented. The mock is doing exactly what you set it up to do. The fact that the specification changed is of no consequence to the mock. The only problem in this scenario is that the person who implemented the change forgot to update the unit tests. I'm actually not too sure why you think there is any duplication of concerns going on.

The coder that is adding some new return result to the system is responsible for adding a unit test to handle this case. If that code is also 100% sure that there is no way that the null result could possibly be returned now, then he could also delete the old unit test. But why would you? The unit test correctly describes the behavior of the object under test when it receives a null result. What happens if you change the backend of your system to some new database that does return a null? What if the specification changed back to returning null? You might as well keep the test, since as far as your object is concerned, it could really get anything back from the external resource, and it should gracefully handle every possible case.

The whole purpose of mocking is to decouple your tests from real resources. It's not going to automatically save you from introducing bugs into the system. If your unit test accurately describes the behavior when it receives a null, great! But this test should not have any knowledge of any other state, and certainly should not be somehow informed that the external resource will no longer be sending nulls.

If you're doing proper, loosely coupled design, your system could have any backend you could imagine. You shouldn't be writing tests with one single external resource in mind. It sounds like you might be happier if you added some integration tests that use your real database, thereby eliminating the mocking layer. This is always a great idea for use with doing a build or sanity/smoke tests, but is usually obstructive for day to day development.

like image 110
womp Avatar answered Nov 13 '22 06:11

womp


You're not missing something here. This is a weakness in unit testing with mock objects. It sounds like you are properly breaking your unit tests down into reasonably sized units. This is a good thing; it's far more common to find people testing too much in a "unit" test.

Unfortunately, when you test at this level of granularity, your unit tests don't cover the interaction between collaborating objects. You need to have some integration tests or functional tests to cover this. I don't really know a better answer than that.

Sometimes it's practical to use the real collaborator instead of a mock in your unit test. For example, if you're unit testing a data access object, using the real domain object in the unit test instead of a mock is often easy enough to set up and performs just as well. The reverse is often not true -- data access objects typically need a database connection, file or network connection and are pretty complicated and time consuming to set up; using a real data object when unit testing your domain object will turn a unit test that takes microseconds into one that takes hundreds or thousands of milliseconds.

So to summarize:

  1. Write some integration/functional testing to catch problems with collaborating objects
  2. It's not always necessary to mock out collaborators -- use your best judgement
like image 40
Don McCaughey Avatar answered Nov 13 '22 04:11

Don McCaughey