@Serializable
@JvmInline
value class EmailString(private val input: String) {
init {
require(input.trim().length >= 3) {
"Minimum length of an email address is 3 characters, but input was $input."
}
require(input.trim().length <= 254) {
"Maximum length of an email address is 254 characters, but input was $input."
}
require(input.contains('@')) {
"Every email address must contain the '@' character, but input was $input."
}
}
val normalized: String
get() = input.lowercase().trim()
companion object {
fun fromUnvalidatedString(s: String?): EmailString? {
...
}
}
}
When we have an argument of this type, and try to mock the call using every with an any() for the email argument, it complains with something like "Every email address must contain the '@' character, but input was 4e2474b11bef6f51." because of the sanity check in the init block. Is there a way to get around this?
It looks like your problem isn’t in the EmailString class shown in the question — which does some sensible validation in its init block — but with how you’re creating it from MockK. Perhaps you could show the code for the latter?
(I don’t know MockK, but I’m guessing it may have some way to add a restriction or specialisation for the random data it generates…? — Unless you want to test the validation, of course, in which case that error is correct.)
It seems to be saying that you can call inviteUser() with any params, and it should always return a mock object. But that’s clearly not true for your code — it won’t return a mock if the validation fails.
So you need to change that line somehow, either to say that you might get an exception instead of a mock, or to say that you will get a mock if you provide a valid-looking email address. (I’m guessing the latter will be more useful.)
As I said, I don’t know MockK, so I don’t know how you’d code that, but I’m guessing there’ll be a way to tweak that middle parameter so that instead of a plain any(), you can restrict or specialise it to generate a valid-looking email address?
For the record, a possible solution is to make the constructor private, provide a “fake constructor” in the companion object and move the validation there. MockK is fine with this, and the behavior for normal code doesn’t change:
@Serializable
@JvmInline
value class EmailString private constructor(private val input: String) {
val normalized: String
get() = input.lowercase().trim()
companion object {
operator fun invoke(input: String) {
require(input.trim().length >= 3) {
"Minimum length of an email address is 3 characters, but input was $input."
}
require(input.trim().length <= 254) {
"Maximum length of an email address is 254 characters, but input was $input."
}
require(input.contains('@')) {
"Every email address must contain the '@' character, but input was $input."
}
EmailString(input)
}
fun fromUnvalidatedString(s: String?): EmailString? {
...
invoke(s)
}
}
}
I think your understanding of mocking is incorrect. Now, I’m no expert on value classes, so I might be about to talk out of my ass here.
When you do every{ UserService.inviteUser(any(), any(), any()) } returns mockk(), you are saying that when UserService.inviteUser() is called with any arguments, it will return a mock. So let’s look at how this would work in your test.
class MyClassBeingTested {
fun createUser(organizationId: Long, email: String, name: String) {
val emailString = EmailString(email) // <----- this line blows up
UserService.inviteUser(organizationId, emailString, name)
}
}
So hopefully what you can see straight away is that a) you haven’t mocked out EmailString, and b) EmailString is created before there’s even any interactions with your mock. In the above code, the exception doesn’t occur when calling UserService.inviteUser, the exception occurs when you create an EmailString with an invalid email address. What you should be doing instead (if possible) is creating a mock EmailString, and passing that to whatever function you’re testing (assuming the function doesn’t accept a String and create an EmailString internally). If your code instantiates an EmailString, then you’re gonna get an exception if it’s not a valid email address, because the init block runs every time you create an EmailString.
I don’t think this is what happens. In the setup I use, there are only valid emails going in, which will not blow up, and the error message shows that it is definitely the any() call which is blowing up. The error message also shows that MockK is using some random String to construct the value classes, as described in the MockK issue linked above. Also note that the problem goes away as soon as EmailString is converted to a normal or a data class.
Moving the validation from the real to a “fake” constructor (invoke() in the companion object) solves the problem, because all normal users go through the fake constructor and get still validated, while MockK goes straight to the real, now private constructor, which works because at this time no more validation is happening.
I’m confident that my code is working, I posted it so that other people running in the same problem have an easy solution, which doesn’t require to change any client code using the class, and allows to keep the value class.
Ah right, I think your initial post wasn’t super clear, but now I understand.
The problem is when you set up the mock; setting up the mock actually calls the function, and using any() passes in a value that’s some kind of argument matcher or something… so I guess the any() invocation is trying to create an EmailString instance. Interesting.
I wonder if you could get around it with an argThat { true }, or something? (I know Mockito Kotlin has argThat, and I’m assuming MockK has something comparable)