Flutter How to write unit test for thrown exception

eye-catchDart and Flutter

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.

Sponsored links

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

Copied title and URL