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?
CodePudding user response:
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