简体   繁体   中英

In Scala, how do I properly unit test a class method that calls other class methods?

I have a case class that represents a phone number, and provides some helper methods that are used elsewhere in the code. One of these methods calls other methods within the same class:

case class PhoneNumber {

  // class data and methods

  def isPossiblyUsable(): Boolean = {
    // both methods return a Boolean
    this.isUsable() || this.usabilityUnknown()
  }
}

I'd like to unit test this method, but I'm not sure what the best approach is. Coming from a Python background, I thought it'd be possible to simply mock PhoneNumber.isUsable and PhoneNumber.usabilityUnknown , but based off of some answers to this question it sounds like you cannot partially mock a class. I decided to try anways:

"PhoneNumber.isPossiblyUsable" should "return true when usable" in {
  val m = mock[PhoneNumber]

  (m.isUsable _).expects().returning(true)
  (m.usabilityUnknown _).expects().returning(false)

  assert(m.isPossiblyUsable() == true)
}

This fails with message:

[info] PhoneNumber.isPossiblyUsable
[info] - should return true when usable *** FAILED ***
[info]   Unexpected call: <mock-1> PhoneNumber.isPossiblyUsable()
[info]   
[info]   Expected:
[info]   inAnyOrder {
[info]     <mock-1> PhoneNumber.isUsable() once (never called - UNSATISFIED)
[info]     <mock-1> PhoneNumber.usabilityUnknown() once (never called - UNSATISFIED)
[info]   }
[info]   
[info]   Actual:
[info]     <mock-1> PhoneNumber.isPossiblyUsable() (Option.scala:201)

Sounds like isPossiblyUsable is not being called in the test. Is there a way to actually call it? Or is my approach here completely off?

You can "partially mock" a class (it's a called a "spy"):

   val p = spy(PhoneNumber("5551212"))
   when(p.isUsable).thenReturn(true)
   when(p.usabilityUnknown).thenReturn(false)
   p.possibleUseable shouldBe true

But you really should not do this. "Spying" is usually a sign of bad design, because it violates the single responsibility principle: the unit you are testing should only have one responsibility, so, you either mock it whole, or you test it in its entirety.

Additionally, mocking/spying case-classes is generally not a good idea too. Case-class is intended to be a simple data container, without external dependencies that normally need to be mocked.

In your case, if isUsable and usabilityUnknown are simple functions of the actual number, then you should not be mocking them at all, just supply the data that causes them to return the desired value.

For example:

    def isUsable() = phone.length=10
    def usabilityUnknown = phone.length=7

Then you can just do

PhoneNumber("9415551212").possiblyUsable shouldBe true
PhoneNumber("5551212").possiblyUsable shouldBe true
PhoneNumber("foo").possiblyUsable shouldBe false

without needing to mock anything at all.

Another possibility is that you have some external "blackbox" component that determines usability of the phone numbers.

In that case, those calls should probably not be a part of the PhoneNumber class to begin with. Instead, it should depend on that service, and then you can mock it. Like so:

case class PhoneNumber(phone: String, usability: PhoneUsabilityService = PhoneUsabilityService) { 
    def possiblyUsable = usability.isUsable(this) || usability.usabilityUnknown(this)
}

Now you can do things like

val u = mock[PhoneUsabilityService]
when(u.isUsable(any)).thenReturn(true)
PhoneNumber("foo", u).possiblyUsable() shouldBe true

Moreover, you can also make sure that conditions are checked in the correct order:

verify(u).isUsable(Phone("foo", u))
verifyNoMoreInteractions(u)

It may also make sense to supply the external component to just the method itself (perhaps, as an implicit parameter), rather than to the class. That seems a bit cleaner to me personally, as it provides a better indication of where this external service is used within the class:

case class PhoneService(phone: String) {
  def possiblyUsable(implicit u: PhoneUsabilityService) = 
    u.isUsable(this) || u.usabilityUnknown(this)
}

PhoneNumber("foo").possiblyUsable(u) shouldBe true

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM