1

Many places in stackexchange state that you should not unit test implementations, only the public interface of a class. But what if the public interface is a method that doesn't return any value, for example a public SendMailNotifications() method with certain logic that has some internal logic (private methods) and sends different recipients emails - I'd like to add tests that email is sent to the correct recipients depending on my business logic.

The only way I could think of is to convert the private methods to protected and then Verify() that they're called. But this seem to contradict the 'spirit' of unit tests where you shouldn't test implementations\private methods (and to convert the private methods to protected only for the sake of testing).

Is there any better way (a way which is more in the 'spirit' of unit tests) to accomplish this? Perhaps the code itself should be rafactored for this purpose? If so, how?

  • 4
    That's what integration tests are for. – Andy Oct 25 '16 at 17:50
  • 1
    Try to encapsulate such business into another component. You are trying.to tests the correctness of such business not the notification Itself. Otherwise what you need is to perform a integration test. – Laiv Oct 25 '16 at 17:59
  • 5
    An important insight is that void methods either modify their parameters or perform side-effects (or both), and parameterless void methods can only perform side-effects! – Andres F. Oct 25 '16 at 18:12
  • 1
    @AndresF. modifying your parameters is a side effect. void methods are always about the side effects – Caleth Oct 25 '16 at 19:17
  • 1
    But thing here is that OP wants to tests a chunck of business aside from the notification... Then move the business out of the NotificationSender. Otherwise. What are you really testing? – Laiv Oct 25 '16 at 20:56
3

Dependency Injection and mocking is your friend. Whenever you find something hard to test, it usually means your class is having too many responsibilities. I'm assuming you are sending your emails using some library. That library most likely takes your message info (destination address, subject, body). What you want to do, is decouple your business logic that's in charge of collecting all email addresses, email subject and body, from that 3rd party library. How? By abstracting that library using an interface, and then injecting it into your Email class.

IEmailLibrary
{
   void SendEmail(EmailInfo emailInfo);
}

class Email
{
   private IEmailLibrary _emailLibrary;

   public Email(IEmailLibrary emailLibrary)
   {
      _emailLibrary = emailLibrary;
   }

   public void SendEmailNotifications()
   {
      EmailInfo emailInfo = ComposeEmail(); 
      _emailLibrary.SendEmail(emailInfo);
   }

   private EmailInfo ComposeEmail()
   {
      //your business logic
   }
}

Now in your production code, you would create a real EmailLibrary like this:

public RealEmailLibrary : IEmailLibrary
{
   ...

   public SendEmail(EmailInfo emailInfo)
   {
      _3rdPartyEmailComponent.SendEmail(emailInfo....);
   }
}

and then inject it into your Email class, like this:

RealEmailLibrary realEmailLibrary = new RealEmailLibrary();
Email email = new email(realEmailLibrary);

And if you wanted to test your business logic, all you need to do in your test code, is inject either a mock, or your own stub that implements an IEmailLibrary interface. For example:

public FakeEmailLibrary : IEmailLibrary
{
   public EmailInfo _iWasCalledWithThisEmailInfo;

   public void SendEmail(EmailInfo emailInfo)
   {
      _iWasCalledWithThisEmailInfo = emailInfo;
   }
}

You inject it in your test like this:

FakeEmailLibrary fakeEmailLibrary = new FakeEmailLibrary();
Email email = new email(fakeEmailLibrary);
email.SendEmailNotifications();
AssertStuff(fakeEmailLibrary.iWasCalledWithThisEmailInfo, expectedEmailInfo);

I recommend using mocking frameworks, as opposed to my stub example (like Moq for C#), which make it really easy to verify your injected dependencies were called with the right arguments.

  • I don't want to say it's completely wrong what you're showing here. Technically, I sure it's all valid. But is it sane? I doubt it. The test case is beyond all unit testing and should be tested as as integration test. The reason is clearly, the implementation of the test object. Why, am I saying this? Reason is simple. You're adding a lot of complexity to the test. Complexity, which might lead to errors and lower the maintainability massively. Just my two cents. – DHN Oct 26 '16 at 8:09
  • 1
    @DHN How is the test case beyond unit testing? He has business logic that has to be tested, and you sure don't want to test it by actually sending an email via an email server, and then verifying the other end got it. Single Responsibility Principle - look it up. The business logic can and SHOULD be tested in isolation. With this approach, you can test multiple edge cases in milliseconds. How much time would it take you to send even a single email instead? This is how things are done in the real world, so you don't end up with a ball of mud that's impossible to test properly. – Eternal21 Oct 26 '16 at 11:59
  • Ok, you're right. Focusing on this it's a unit test case. But still I've some doubts, if you have to spend a lot of effort for mocking. Perhaps, I'm just a little bit lazy. ;o) – DHN Oct 26 '16 at 13:04
  • 1
    @DHN I wouldn't recommend this approach for simple 'Hello World' type projects, that you'll write once and throw away. But if you want to build an commercial application, that needs to be extended and maintained for years, then this is the only way of ensuring you won't end up with a mess in the long run. Look into Dependency Injection, and how it relates to TDD, and unit testing in general. You'll find it will make your job much easier. – Eternal21 Oct 26 '16 at 14:23
  • 1
    @BornToCode You shouldn't need to change any members from private to protected, when you're injecting them into your classes. – Eternal21 Nov 20 '16 at 23:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.