I didn’t know how to write unit test for a function that throws an exception. Therefore. I looked into the solution and will write the same thing in different ways. See how many options we have and choose one depending on your preference.
What should I specify to matcher property
We need to understand how Dart framework checks the two object. If you check the official page you see a useful package list.
https://dart.dev/guides/testing#generally-useful-libraries
package:test is used in this article. expect
function is defined in the following way.
void expect(
dynamic actual,
dynamic matcher,
{String? reason,
dynamic skip}
)
We specify an actual value to the first argument and an expected value to the second argument. We need to if the expected value is primitive value or class we can simply pass the variable to it. If the tested function throws an error we need to prepare the proper matcher. matcher
property is used to call wrapMatcher
function in expect
function.
Matcher wrapMatcher(x) {
if (x is Matcher) {
return x;
} else if (x is bool Function(Object?)) {
// x is already a predicate that can handle anything
return predicate(x);
} else if (x is bool Function(Never)) {
// x is a unary predicate, but expects a specific type
// so wrap it.
// ignore: unnecessary_lambdas
return predicate((a) => (x as dynamic)(a));
} else {
return equals(x);
}
}
For primitive value and class value, it reaches to else clause but equals
function can’t be used for checking an exception. Therefore, we need to create the proper matcher. Let’s see how we can test an exception by using matcher.
Checking the error data type
I defined the following function. It just throws an ArgumentError.
Never throwError() {
throw ArgumentError(
"Error message.",
"invalidName",
);
}
There are several ways to write the test. The following 4 ways return the same result.
test('should throw ArgumentError', () {
expect(throwError, throwsA(isInstanceOf<ArgumentError>()));
expect(throwError, throwsA(isA<ArgumentError>()));
expect(throwError, throwsA(isArgumentError));
expect(throwError, throwsA(predicate((x) => x is ArgumentError)));
});
The first 3 ways are actually the same. Look at the actual implementations. They all eventually create TypeMatcher
instance.
TypeMatcher<T> isInstanceOf<T>() => isA<T>();
TypeMatcher<T> isA<T>() => TypeMatcher<T>();
const isArgumentError = TypeMatcher<ArgumentError>();
If the error is not our own error, we can use isXXXXError
which is predefined and the most readable. If the error is our own error, isA<T>
can be the next choice because it is shorter than isInstanceOf
.
By the way, what is throwsA
function? The implementation is following.
Matcher throwsA(matcher) => Throws(wrapMatcher(matcher));
class Throws extends AsyncMatcher {
...
const Throws([Matcher? matcher]) : _matcher = matcher;
...
]
It creates Throws
class that extends AsyncMatcher. wrapMatcher
function has already been shown above. I show it here again.
Matcher wrapMatcher(x) {
if (x is Matcher) {
return x;
} else if (x is bool Function(Object?)) {
// x is already a predicate that can handle anything
return predicate(x);
} else if (x is bool Function(Never)) {
// x is a unary predicate, but expects a specific type
// so wrap it.
// ignore: unnecessary_lambdas
return predicate((a) => (x as dynamic)(a));
} else {
return equals(x);
}
}
isInstanceOf
, isA
and isArgumentError
is TypeMatcher
class which extends Matcher
class.
class TypeMatcher<T> extends Matcher {
...
}
Therefore, wrapMatcher
execute if clause which just returns x. The last way creates a function to check if the incoming value fulfill the condition.
expect(throwError, throwsA(predicate((x) => x is ArgumentError)));
Since its return data type is bool, else-if clause in wrapMatcher
is executed. So we can define our own logic for the comparison in predicate function.
Checking if the error has expected error message
We often want to check if the error message has an expected error message. How can we check it? As I explained above, we can define our own comparing logic in predicate function.
test('should throw ArgumentError with message', () {
expect(
throwError,
throwsA(
predicate(
(x) => x is ArgumentError && x.message == "Error message.",
),
));
});
We have another option. The second argument is just a description for readability.
expect(
throwError,
throwsA(
isArgumentError.having(
(x) => x.message,
"my message",
contains("Error"),
),
));
We can check other properties as well.
test('should throw ArgumentError with invalid value and property name', () {
expect(
throwError,
throwsA(
predicate(
(x) => x is ArgumentError && x.name == "invalidName",
),
));
});
If using a named constructor, be aware that the order is different.
ArgumentError([this.message, @Since("2.14") this.name])
: invalidValue = null,
_hasValue = false;
ArgumentError.value(value, [this.name, this.message])
: invalidValue = value,
_hasValue = true;
Comments