2

I am new to practicing TDD and I am faced with a design problem in writing what seems a straightforward unit test.

The method under test does two things:

  1. Calls a private method that converts object A into object B
  2. Calls another private method passing object B as an argument

Something like this:

public void doStuff(A objectA) { B objectB = convertToB(objectA); processB(objectB); }

Now, where do I test if the conversion was done correctly? The popular opinion says that using TDD there is no need in using PowerMock or other libraries to test private methods. I quote Practical Unit Testing with JUnit and Mockito book:

The first thing you could, and probably should, do, is to avoid such a situation. How? By following the TDD approach. Think about how private methods come to life when you code test first. The answer is, that they are created during the refactoring phase, which means their content is fully covered by tests (assuming that you really follow the TDD rules, and write code only when there is a failing test). In such cases there is no problem of an "untested private method which should somehow be tested", because such methods simply do not exist.

My next idea was to use an ArgumentCaptor and verify if processB() was called with correct argument. But again, processB() is private, too, so it cannot be done.

Of course, a lot of tricks can be done to make my class testable - save objectB in a class field or make one of my private methods public. But that will worsen, not improve my design.

So, my questions in this case are: What is the correct way to test the conversion method? What design improvement can make this code testable?

EDIT: Adding a real-world example to give a better picture of the problem:

public class EmailSender {

    public EmailResult send(Email email) {
        MultivaluedMapImpl formData = prepareFormData(email);
        EmailResult emailResult = processEmailRequest(formData);
    }    

    private MultivaluedMapImpl prepareFormData(Email email) {
        MultivaluedMapImpl formData = new MultivaluedMapImpl();
        formData.add(FROM_KEY, email.getSender());
        email.getRecipients().stream().forEach((recipient) -> {
            formData.add(TO_KEY, recipient);
        });
        formData.add(SUBJECT_KEY, email.getSubject());
        formData.add(TEXT_KEY, email.getText());

        return formData;
    }

    private EmailResult processEmailRequest(MultivaluedMapImpl formData ) {
        Client client = Client.create();
        client.addFilter(new HTTPBasicAuthFilter("api", "API_KEY"));
        WebResource webResource = client.resource(API_URL);
        ClientResponse clientResponse = webResource.type(MediaType.APPLICATION_FORM_URLENCODED).
           post(ClientResponse.class, formData);
        String resultString = clientResponse.getStatusInfo().getFamily().toString();
        EmailResult emailResult = resultString.equals("SUCCESSFUL") ? EmailResult.SUCCESS : EmailResult.FAILED;
        return emailResult;
    }
}

Here, prepareFormData() corresponds to the conversion method in the previous example. What I try to test is if the conversion is correct.

Limbo Exile
  • 1,321
  • 2
  • 21
  • 41

3 Answers3

3

The idea behind TDD is to test features, not methods. So the question

What is the correct way to test the conversion method?

is not the one you should be asking, if you want to use TDD approach.

What design improvement can make this code testable?

This is a better question. To make a proper test for doStuff(A objectA), you need to go back to the specs: what is doStuff supposed to do? The fact, that its return type is void, makes things a little harder to visualize, but let's assume that it does one of the following:

  1. interacts with an external system (file, database, url, etc.)
  2. modifies internal system's state (variables)

In the first case, the verification can be done by mocking the external system and verifying mock interaction; in the second, we should be able to verify the result directly. Anyhow, you need to determine a specific result (let's call it C) and a way to test it for every kind of input A. Your tests should then have the following structure:

  • doStuff(A1) should produce result C1
  • doStuff(A2) should produce result C2
  • etc.

The result C is always dictated by object B, which in turn is determined by object A. So if convertToB() is broken, then the result of the test should not correspond to the expected value and fail. Hence, your conversion method will be covered by tests.

EDIT:

I'll illustrate my point using the real-world example that you have provided.

1) First of all, Client is an external dependency and as such must be mocked for proper unit testing. To do this, you need to get rid of static dependency Client client = Client.create()` and replace it with a constructor or setter injection. Here is a detailed example of haw to do it.

2) Now we are able to mock the client:

Client mockClient = Mockito.mock(Client.class, Mockito.RETURN_DEEP_STUBS);
WebResource mockWebResource = Mockito.mock(WebResource.class);
Mockito.doReturn(mockWebResource).when(mockClient).resource(Mockito.anyString()); //assuming API_URL is a string
EmailSender sender = new EmailSender(mockClient);

3) Prepare a concrete test case :

// actual email details 
Email email = new Email();
email.setSender("john@domain.com");
email.setRecipients("chris@domain.com", "bob@domain.com");
//etc.  

4) Execute test code

sender.send(email);

5) Verify result

// capture parameter
ArgumentCaptor<MultivaluedMapImpl> argument = ArgumentCaptor.forClass(MultivaluedMapImpl.class.class);
Mockito.verify(mockWebResource, Mockito.times(1)).post(Mockito.any(Class.class), argument.capture());
Assert.assertEqual(email.getSender(), argument.getValue().get(FROM_KEY);
Assert.assertEqual(email.getRecipients(), argument.getValue().get(TO_KEY);
// etc.

Note that SUCCESS or FAILED result that you return is irrelevant, because it is not the responsibility of EmailSender class, but rather of Client class, therefore it should not be tested in EmailSenderTest.

Community
  • 1
  • 1
noscreenname
  • 3,314
  • 22
  • 30
  • Your answer makes sense, but does it cover all the cases? Example: Class under test sends emails. It converts Email object of its domain to the format indicated in the API of external service. Results: C1 is "SENT", C2 is "ERROR". Let's say my conversion method contains bugs and it transforms "john@domain.com" into "chris@domain.com". External API will still be able to send it and return C1 - "SENT". I will assume that everything is tested and leave the bug in the code. Am I missing something? – Limbo Exile Aug 17 '16 at 10:55
  • @LimboExile If in this example the recipient of the mail is important, then the result C should include this information, so C1 should be "SENT to john@domain.com". Generally speaking, the more detailed your tests cases are, the better. – noscreenname Aug 17 '16 at 11:01
  • But I don't decide the result C. It comes from an external service that I have no control over. Moreover, if I add input data to the result, I will be able to check only after sending an email to the wrong recipient. – Limbo Exile Aug 17 '16 at 11:14
  • @LimboExile In a test you **must** "decide the result C", because if you don't, then you test is not repeatable (see F.I.R.S.T. principle). In the case of an external system, this can be achieved using mocks. If you provide more details for `processB()`, I'll be happy to show how. – noscreenname Aug 17 '16 at 11:23
  • I provided the details in an example that is much less abstract. What I was trying to say in the previous comment is even if I decide the result C, my options are somewhat limited by the external system. – Limbo Exile Aug 17 '16 at 12:02
  • Thanks for such a detailed explanation. Could you elaborate on this line: `Mockito.verify(mockWebResource, Mockito.times(1)).mockWebResource(Mockito.any(Class.class), argument.capture());` Where does this `mockWebResource()` method come from? – Limbo Exile Aug 17 '16 at 14:13
  • @LimboExile That's a copy paste typo, I fixed it in the answer. Supposed to be `Mockito.verify(mock‌​WebResource, Mockito.times(1)).post(Mockito‌​.any(Class.class), argument.capture());‌`. I'm guessing that `WebResource.getType()` returns a `WebResource`, if not, you might need another level of mocking. – noscreenname Aug 17 '16 at 14:26
1

Here is a complete (and working) set of tests for the (unmodified) EmailSender class:

import javax.ws.rs.core.*;
import com.sun.jersey.api.client.*;
import com.sun.jersey.api.client.WebResource.Builder;
import static email.EmailSender.*;
import mockit.*;
import static org.junit.Assert.*;
import org.junit.*;

public class EmailSenderTest {
    @Tested EmailSender emailSender;
    @Mocked Client emailClient;
    @Mocked ClientResponse response;
    Email email;

    @Before
    public void createTestEmail() {
        email = new Email();
        email.setSender("john@domain.com");
        email.setRecipients("chris@domain.com", "bob@domain.com");
        email.setSubject("Testing");
        email.setText("Just a test");
    }

    @Test
    public void successfullySendEmail() {
        new Expectations() {{
            response.getClientResponseStatus(); result = ClientResponse.Status.OK;
        }};

        EmailResult result = emailSender.send(email);

        new Verifications() {{
            // Verifies correct API URL and media type:
            Builder bldr = emailClient.resource(API_URL).type(
                MediaType.APPLICATION_FORM_URLENCODED);

            // Verifies correct form data:
            MultivaluedMap<String, String> formData;
            bldr.post(ClientResponse.class, formData = withCapture());
            assertEquals(email.getSender(), formData.getFirst(FROM_KEY));
            assertEquals(email.getRecipients(), formData.get(TO_KEY));
            assertEquals(email.getSubject(), formData.getFirst(SUBJECT_KEY));
            assertEquals(email.getText(), formData.getFirst(TEXT_KEY));
        }};

        assertSame(EmailResult.SUCCESS, result);
    }

    @Test
    public void failToSendEmail() {
        new Expectations() {{
            response.getClientResponseStatus();
            result = ClientResponse.Status.NOT_FOUND;
        }};

        EmailResult result = emailSender.send(email);

        // No need to repeat here the verification for URL, form data, etc.
        assertSame(EmailResult.FAILED, result);
    }
}

These two tests should be sufficient to fully cover the class under test. Also, they should be able to detect any bugs that may exist anywhere in the implementation of EmailSender.

Note each test covers one of two "business scenarios": a) the email is sent by the email client which receives a result of "success", or b) the email is sent but the email client gets a "failure" result of some kind. The first test also checks for important details in the act of sending the email. All these things are dictated by the requirements of the unit under test. Among said requirements is the correct interaction with the external Client dependency, which is being mocked.

Rogério
  • 16,171
  • 2
  • 50
  • 63
-1

If you have already written a method and now you're trying to test it, you're not practicing TDD. With TDD, you write the test first. Now you don't have the problem with methods that are hard to test, because whatever methods you write already have tests.

Mike Stockdale
  • 5,256
  • 3
  • 29
  • 33