Unit testing a class which uses DI without testing on internals

I have a class which is refactored in 1 main class and 2 smaller classes. The main classes use the database (like a lot of my classes do) and sends an email.
So the main class has an IPersonRepository and an IEmailRepository injected which in his turn sends to the 2 smaller classes.

Now I want to unit test the main class, and have learned not to unittest the internal workings of the class, because we should be able to change the internal workings without breaking unit tests.

But as the class uses the IPersonRepository and an IEmailRepository, I HAVE to specify (mock / dummy) results for some methods for the IPersonRepository. The main class calculates some data based on existing data and returns that. If I want to test that, I don’t see how I can write a test without specifying that the IPersonRepository.GetSavingsByCustomerId returns x. But then my unit test ‘knows’ about the internal workings, because it ‘knows’ which methods to mock and which not.

How can I test a class which has injected dependencies, without the test knowing about the internals?

background:

In my experience lots of tests like this create mocks for the repositories and then either provide the right data for the mocks or test if a specific method was called during execution. Either way, the test knows about the internals.

Now I’ve seen a presentation about the theory (which I’ve heard before) that the test should not know about the implementation. First because you are not testing how it works, but also because when you now change the implementation all unit tests fail because they ‘know’ about the implementation. While I like the concept of the tests being unaware of the implementation, I don’t know how to accomplish it.

1

The main class calculates some data based on existing data and returns that. If I want to test that, I don’t see how I can write a test without specifying that the IPersonRepository.GetSavingsByCustomerId returns x. But then my unit test ‘knows’ about the internal workings, because it ‘knows’ which methods to mock and which not.

You are right that this is violation of the “do not test internals” principle and is a common one that people overlook.

How can I test a class which has injected dependencies, without the test knowing about the internals?

There are two solutions that you can adopt to work around this violation:

1) Provide a complete mock of IPersonRepository. Your currently described approach is to couple the mock to the inner workings of the method under test by only mocking the methods it will call. If you supply mocks for all the methods of IPersonRepository, then you remove that coupling. The inner workings can change without affecting the mock, thus making the test less brittle.

This approach has the advantage of keeping the DI mechanism simple, but it can create a lot of work if your interface defines many methods.

2) Do not inject IPersonRepository, either inject the GetSavingsByCustomerId method, or inject a savings value. The problem with injecting entire interface implementations is that you are then injecting (“tell, don’t ask”) an “ask, don’t tell” system, mixing up the two approaches. If you take a “pure DI” approach, the method should be provided (told) the exact method to call if it wants a savings value, rather than being given an object (which it must then effectively ask for the method to call).

The advantage of this approach is that it avoids the need for mocks (beyond test methods that you inject into the method under test). The disadvantage is that it can cause method signatures to change in response to the requirements of the method changing.

Both approaches have their pros and cons, so pick the one that suits your needs best.

1

My Approach is to create ‘mock’ versions of the repositories which read from simple files which contain the needed data.

This means the individual test doesn’t ‘know’ about the setup of the mock, although obviously the overall test project will reference the mock and have the setup files etc.

This avoids the complex mock object setup required by mocking frameworks and allows you to use the ‘mock’ objects in real instances of your application for UI testing and the like.

Because the ‘mock’ is fully implemented, rather than specifically setup for your test scenario, a change of implementation, for example lets say that GetSavingsForCustomer should now also delete the customer. Won’t break the tests (unless of course it really breaks the tests) you need only update your single mock implementation and all your tests will run against it without changing their setup

2

Unit tests are usually whitebox-tests (you have access to the real code). Therefore it is ok to know internals to a certain extent, however for beginners it is easier to not, because you should not test internal behavior (such as “calling method a first, then b and then a again”).

Injecting a mock that provides data is ok, as your class (unit) depends on that external data (or data-provider). However, you should verify the results (not the way to get to them)! e.g. you provide a Person instance and verify that an email was sent to the correct email-address, e.g. by providing a mock for the email, too, that mock does nothing but store the addressee’s e-mail-address for later access by your test-code. (I think Martin Fowler calls them Stubs rather than Mocks, though)

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *