shouldly logo

pub package codecov likes style: lint Dart

shouldly is an assertion concept that focuses on

  • writing assertions as plain English sentences
  • and giving better error messages when the assertion fails while being simple and terse.

Inspired from Fluent Assertion, Shouldly, should.js

Features

  • More readable test code
  • Better test failure messages
  • Conjunction support (and)
  • Custom assertions

Better readability

More readable test code as plain English sentence.

void main() {
  const playerCharacter = PlayerCharacter(
    'Arthur',
    100,
    ['Axe', 'Sword', 'Staff of Wonder'],
  );

  test('equality', () {
    // non shouldly
    expect(playerCharacter.health, 100);
    // 😍 shouldly
    playerCharacter.health.should.be(100);
  });

  test('array contains', () {
    // non shouldly
    expect(playerCharacter.weapons, contains('Staff of Wonder'));
    // 😍 shouldly
    playerCharacter.weapons.should.contain('Staff of Wonder');
  });

  test('nullability of string', () {
    // non shouldly
    expect(playerCharacter.nickname, isNotNull);
    // 😍 shouldly
    playerCharacter.nickname.should.not.beNullOrEmpty();
  });
}

Better test failure messages

To get more readable and contextual information.

Non-Shouldly Shouldly


Expected: <10>
  Actual: <100>


Expected playerCharacter.health
    should be
10
    but was
100


Expected: null
  Actual: 'Arthur'


Expected nickname
    should be null or empty
but was
    `Arthur`


Expected: contains 'Staff of Wonderr'
  Actual: ['Axe', 'Sword', 'Staff of Wonder']


Expected playerCharacter.weapons
    should contain
Staff of Wonder
    but was actually
[Axe, Sword]

No more: Mix Up with parameters

You can mix up with Expected or Actual 🤔. But with shouldly there is no way to mix up.

// without shouldly
expect(playerCharacter.health, 0);
expect(0, playerCharacter.health);

// shouldly
playerCharacter.health.should.be(0);

No more single heap of assertion methods

Before

You could write such crazy assertions.

expect(2, isTrue);
expect(2, contains(2));
expect(2, startsWith('1'));
expect(2, isEmpty);
expect(2, throwsException);

After

Every single type of class has his own assertions. Easy to find required assertion method.

Conjunctions

This is a real English sentence, is it not?

13.should.beOdd().and.beGreaterOrEqualThan(13);
participants.should.contain('Andrew').and.not.contain('Bobby');

SatisfyAllConditions

satisfyAllConditions will show all errors at once, not only first fail assertion.

test('should satisfy all conditions', () {
    Should.satisfyAllConditions([
      () => playerCharacter.nickname.should.as('nickname').not.beNullOrEmpty(),
      () => playerCharacter.weapons.should.as('weapons').contain('Staff of Wonder'),
      () => playerCharacter.health.should.as('health').be(100),
    ]);
  });

Example report can be:

Expected satisfy all conditions specified, but does not.
The following errors were found ...

------------- Error 1 -------------
Expected nickname
    should not be null or empty
but was
    ''

------------- Error 2 -------------
Expected weapons
    should contain
Staff of Wonder
    but was actually
[Axe, Sword]

------------- Error 3 -------------
Expected health
    should be
100
    but was
99

------------------------------------

Custom matchers

For existing one - just create an extension for it

extension CustomNumAssertions on NumericAssertions {
  NumericAssertions get beNegative {
    if (subject >= 0) {
      throw ShouldlyTestFailureError('Number\n  should be negative');
    }
    return NumericAssertions(subject);
  }
}

Or more exotic matchers - fot custom types

  1. First, create a Assertions class for your custom type which extends BaseAssertions
class CustomerAssertions extends BaseAssertions<Customer, CustomerAssertions> {
  // ...

  CustomerAssertions get beMarried {
    if (isReversed) {
      if (subject!.isMarried) {
        throw ShouldlyTestFailureError('Customer should not be married');
      }
    } else {
      if (!subject!.isMarried) {
        throw ShouldlyTestFailureError('Customer should be married');
      }
    }
    return CustomerAssertions(subject);
  }

  CustomerAssertions get beMale {
    if (isReversed) {
      Execute.assertion
          .forCondition(subject!.gender == Gender.male)
          .failWith('$subjectLabel should not be male');
    } else {
      Execute.assertion
          .forCondition(subject!.gender != Gender.male)
          .failWith('$subjectLabel should be male');
    }

    Execute.assertion
        .forCondition(subject!.gender != Gender.male)
        .failWith('$subjectLabel should be male');

    return CustomerAssertions(subject);
  }

  // ...
}
  1. Second, create an extension on your class which has should property with type of your custom assertions class
extension CustomerExtension on Customer {
  CustomerAssertions get should => CustomerAssertions(this);
}
  1. Third, you are ready to go use your own shouldly assertions
test('Custom matchers', () {
  bobby.should.beMale.and.beMarried;
  kate.should.beMarried.and.not.beMale;
});

Complete in

To verify how long your function is executed

test('should complete in a duration', () async {
  await Should.completeIn(
    Duration(seconds: 1),
    func: () => slowFunction(
      Duration(milliseconds: 900),
    ),
  );
});

test('should complete after a period of time', () async {
  await Should.completeAfter(
    const Duration(seconds: 3),
    func: () => verySlowFunction(
      const Duration(seconds: 4),
    ),
  );
});

Getting started

Simple add shouldly dependency into your project.

dev_dependencies:
  shouldly: <latest>

Usage

Objects

Every single object has following assertion methods:

MethodExampleFailure message
be1.should.be(2);Expected int should be 2 but was 1
beOfType2.0.should.beOfType<double>();
beAssignableTo3.should.beAssignableTo<int>();
beNullnull.should.beNull();
beOneOf5.should.beOneOf([1, 2, 5]);
test('should be not null', () {
  final obj = Object();
  obj.should.not.beNull();
});

test('should be null', () {
  const Object? obj = null;
  obj.should.beNull();
});

test('should be type of `int`', () {
  const obj = 1;
  obj.should.beOfType<int>();
});

test('should be assignable to `num`', () {
  const obj = 1;
  obj.should.beAssignableTo<num>();
});

Booleans

MethodExampleFailure message
beTruefalse.should.beTrue();Target boolean should be True but was False
beFalsefalse.should.not.beFalse();Target boolean should not be False but was False
test('false should be `false`', () {
  false.should.beFalse();
});

test('false should not be `true`', () {
  false.should.not.beTrue();
});

Numbers

MethodExampleFailure message
bePositive(-1).should.bePositive();Target int should be positive but was negative as -1
beNegative1.should.beNegative();Target int should be negative but was positive as 1
beZero10.should.beZero();Target int should be 0 but was 10
beOdd8.should.beOdd();Target int should be odd but was even as 8
beEven7.should.beEven();Target int should be event but was odd as 7
beGreaterThan1.should.beGreaterThan(2);Target int should be greater than 2 but does not
beAbove3.should.beAbove(2);
beLessThan3.should.beLessThan(4);
beBelow3.should.beBelow(4);
beGreaterOrEqualThan3.should.beGreaterOrEqualThan(3);
beLessOrEqualThan3.should.beLessOrEqualThan(3);
beWithin3.should.beWithin(1,5);
beCloseTopi.should.beCloseTo(3.14, delta: 0.01);
beTolerantOfpi.should.beTolerantOf(3.14, tolerance: 1%);
test('Int should be type of `int`', () {
  2.should.beEven();
  10.should.beGreaterThan(9);
  9.99.should.not.beCloseTo(10.0, delta: 0.01);
});

Strings

MethodExampleFailure message
startWith'Flutter'.should.startWith('f');
endWith'Flutter'.should.endWith('a');
haveLength'Flutter'.should.haveLength(10);
beNullOrEmpty'Flutter'.should.beNullOrEmpty();
beNullOrWhiteSpace'Flutter'.should.beNullOrWhiteSpace();
beBlank'Flutter'.should.beBlank();
match'Flutter'.should.match('*a');
contain'Flutter'.should.contain('a');
test('should not start with substring', () {
  'Flutter'.should.not.startWith('A');
});

DateTimes

MethodExampleFailure message
beCloseToDateTime.now().should.beCloseTo(Date(2025, 1, 1));
beAfterDateTime.now().should.beCloseTo(Date(2022, 1, 1));
beBeforeDateTime.now().should.beBefore(Date(2222, 1, 1));
// before
DateTime(2021, 9, 9).should.beBefore(DateTime(2021, 9, 10));
DateTime(2021, 9, 9).should.not.beBefore(DateTime(2021, 9, 9));

// close to
DateTime(2021, 9, 9, 1, 1, 1, 2).should.beCloseTo(
      DateTime(2021, 9, 9, 1, 1, 1, 3),
      delta: Duration(milliseconds: 1),
    );

Iterables

MethodExampleFailure message
haveCount[1, 2, 3].should.haveCount(7);
contain[1, 2, 3].should.contain(7);
containAll[1, 2, 3].should.containAll([7, 8]);
every[1, 2, 3].should.every((x) => x > 10);
any[1, 2, 3].should.any((x) => x == 0);
test('should contain', () {
  [1, 200, 3].should.contain(200);
});

test('should not contain', () {
  [1, 2, 4].should.not.contain(3);
});

test('with every element in collection is true for predicate', () {
  [3, 5, 7, 9].should.every((item) => item < 10);
});

test('with some elements in collection is true for predicate', () {
  [3, 5, 7, 9].should.any((item) => item > 8);
});

Maps

MethodExampleFailure message
haveCount{}.should.haveCount(7);
beEmpty{}.should.beEmpty();
containKey{'name': 'Bobby'}.should.containKey('age');
containKeys{}.should.containKeys(['age', 'name']);
haveValueInKey{}.should.haveValueInKey('name');
containKeyWithValue{}.should.containKeyWithValue('name', 'Bobby');
contain{}.should.haveValueInKey(<MapEntry>[]);
final subject = {
  'name': 'John',
  'age': 18,
};

test('should contain key', () {
  subject.should.containKey('name');
});

test('should contain key with exact value', () {
  subject.should.containKeyWithValue('age', 18);
});

Functions

MethodExampleFailure message
throwExceptionShould.throwException(() => myFunc());
notThrowExceptionShould.notThrowException(() => myFunc());
throwErrorShould.throwError(() => myFunc());
notThrowErrorShould.notThrowError(() => myFunc());
completeInShould.completeIn(Duration(seconds: 1), () => myFunc());
Should.throwException(() => someMethodWitchThrowException(params:));
Should.throwError<ExactError>(() => someMethodWitchThrowExactError(params:));
test('async function should throw exception', () async {
  await Should.throwAsync(() {
    Future.delayed(Duration(milliseconds: 100));
    throw Exception('test');
  });
});

test('async function should throw exact exception', () async {
  await Should.throwAsync<CustomException>(() {
    Future.delayed(Duration(milliseconds: 100));
    throw CustomException('custom exception test');
  });
});

test('should complete in a duration', () async {
  await Should.completeIn(
    Duration(seconds: 1),
    func: () => slowFunction(
      Duration(milliseconds: 900),
    ),
  );
});

Enums

test('should not be equal', () {
  seasons.spring.should.not.be(seasons.winter);
});

test('should not be type of', () {
  seasons.spring.should.not.beOfType<level>();
});

test('should be assignable to `Enum`', () {
  seasons.spring.should.beAssignableTo<Enum>();
});

More examples here

Writing Custom Matchers

extension CustomerExtension on Customer {
  CustomerAssertions get should => CustomerAssertions(this);
}

class CustomerAssertions extends BaseAssertions<Customer, CustomerAssertions> {
  CustomerAssertions(
    Customer? subject, {
    bool isReversed = false,
    String? subjectLabel,
  }) : super(subject, isReversed: isReversed, subjectLabel: subjectLabel);

  CustomerAssertions get beMarried {
    if (isReversed) {
      if (subject!.isMarried) {
        throw ShouldlyTestFailure('Customer should not be married');
      }
    } else {
      if (!subject!.isMarried) {
        throw ShouldlyTestFailure('Customer should be married');
      }
    }
    return CustomerAssertions(subject);
  }

  CustomerAssertions get beMale {
    if (isReversed) {
      if (subject!.gender == Gender.male) {
        throw ShouldlyTestFailure('Customer should be female');
      }
    } else {
      if (subject!.gender != Gender.male) {
        throw ShouldlyTestFailure('Customer should be male');
      }
    }

    return CustomerAssertions(subject);
  }

  @override
  CustomerAssertions copy(
    Customer? subject, {
    bool isReversed = false,
    String? subjectLabel,
  }) =>
      CustomerAssertions(
        subject,
        isReversed: isReversed,
        subjectLabel: subjectLabel,
      );
}

Recommendations

You can improve the readability of the rest of your test code with given_when_then_unit_test, which enhances the test report readability as well.

drawing

Changelog

Please see the Changelog page to know what's recently changed.

Contributing

Feel free to contribute to this project.

If you find a bug or want a feature, but don't know how to fix/implement it, please fill an issue.
If you fixed a bug or implemented a new feature, please send a pull request.

We accept the following contributions:

  • Ideas how to improve
  • Reporting issues
  • Fixing bugs
  • More tests
  • More class integrations (Functions, Futures, Streams, Durations)
  • Improving documentation and comments

Maintainers

Libraries

shouldly
A simple, extensible, readable library for assertions.
shouldly_bool
shouldly_collection
shouldly_datetime
shouldly_enum
shouldly_function
shouldly_map
shouldly_num
shouldly_object
shouldly_string