0

I am learning Scala and wrote Email class which looks like:

class Email(emailConfigName: String) {
  private val emailConfig = ConfigFactory.load(emailConfigName) getConfig ("email")

  def notifySupportForTenant(symbolicName: String) {
    val emailMessage: Message = constructEmailMessage(symbolicName)
    Transport.send(emailMessage)
  }

  def constructEmailMessage(symbolicName: String): Message = {
    val message = createMessage(emailConfig getString ("shn.mail.smtp.host"))
    message setFrom (new InternetAddress(emailConfig getString ("shn.mail.from")))
    // todo: should come from API (sent by client)
    message setSentDate (new Date())
    message setSubject (emailConfig getString ("shn.mail.subject") replace("TENANT_ID", symbolicName))
    message setContent(emailConfig getString ("shn.mail.body"), "text/html")
    message.setRecipients(Message.RecipientType.TO, getMessageRecipients(emailConfig getString ("shn.mail.recipient")))
    message
  }

  private def createMessage(smtpHost: String): Message = {
    val properties = new Properties()
    properties put("mail.smtp.host", smtpHost)
    val session = Session.getDefaultInstance(properties, null)
    return new MimeMessage(session)
  }

  private def getMessageRecipients(recipient: String): Array[Address] = {
    // had to do the asInstanceOf[...] call here to make scala happy
    val addressArray = buildInternetAddressArray(recipient).asInstanceOf[Array[Address]]
    if ((addressArray != null) && (addressArray.length > 0)) addressArray
    else
      throw new IllegalArgumentException("no recipients found to send email")
  }

  private def buildInternetAddressArray(address: String): Array[InternetAddress] = {
    // could test for a null or blank String but I'm letting parse just throw an exception
    return InternetAddress.parse(address)
  }
}

I want to test this class's public API, notifySupportForTenant but this is not good for unit test since it also calls Transport.send(emailMessage) which will send the email.

All I am interested in testing if the message is constructed correctly. Which means I need to test constructEmailMessage.

In order to test this, I had to make this public which is also exposed as public interface, which I do not prefer. What can I do?

kryger
  • 12,906
  • 8
  • 44
  • 65
daydreamer
  • 87,243
  • 191
  • 450
  • 722
  • have you tried reflection? – Epicblood Jul 30 '15 at 22:00
  • 1
    One common way out is to make the private methods package-private, and then put the tests in the same package. – Seth Tisue Jul 30 '15 at 22:05
  • 2
    possible duplicate of [How to test a class that has private methods, fields or inner classes](http://stackoverflow.com/questions/34571/how-to-test-a-class-that-has-private-methods-fields-or-inner-classes) – Egor Jul 30 '15 at 22:08

2 Answers2

6

In traditional OO programming methods are often made private so that they cannot be called by the outside and corrupt the state of the instance. Once your method is really a function without side-effects (as constructEmailMessage appears to be), there really isn't a reason to make it private.

As your purpose is to learn scala, I would encourage you to adopt a functional style operating on immutable data as much as possible, because it alleviates a lot of the reasons OO programming favors protected encapsulations.

Arne Claassen
  • 14,088
  • 5
  • 67
  • 106
  • Agreed! The `Email` class sounds like the perfect thing to make into a `case class`! – childofsoong Jul 30 '15 at 22:50
  • Off the top of my head, 2 reasons for wanting to keep a function private: 1) you don't wan't to clutter the public API, 2) it is a helper function that you want to be able to freely refactor / rename without worrying about breaking client software. I'd pref Seth Tissue's recommendation and scope the method to `private[mypackage] def createMesssage(...` – Mark S Aug 01 '15 at 15:14
  • You can achieve both 1 & 2 by putting the public API in a trait, leaving your implementation to keep methods public (as long as they're stateless & idempotent). If you are worried about the API surface, package private still leaves the next maintainer of your package to accidentally use your private method. Personally, I prefer splitting public API and implementation as it is more explicit. Either works, tho – Arne Claassen Aug 01 '15 at 17:02
0

You can use PowerMock library!

import org.powermock.reflect.Whitebox

val email = new Email("lalala@mail.com")
val expected = new Message(...)
val smtpHost = "127.0.0.1"

val actual = Whitebox.invokeMethod(email, "createMessage", smtpHost).asInstanceOf[Message]

assert(actual = expected)

This library also helpful for mocking private methods, but if you don't need its entire power for you tasks you can use reflection as mentioned in this post.

Community
  • 1
  • 1
ka4eli
  • 5,294
  • 3
  • 23
  • 43