 
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
- 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);
  }
  // ...
}
- Second, create an extension on your class which has shouldproperty with type of your custom assertions class
extension CustomerExtension on Customer {
  CustomerAssertions get should => CustomerAssertions(this);
}
- Third, you are ready to go use your own shouldlyassertions
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:
| Method | Example | Failure message | 
|---|---|---|
| be | 1.should.be(2); | Expected intshould be 2 but was 1 | 
| beOfType | 2.0.should.beOfType<double>(); | |
| beAssignableTo | 3.should.beAssignableTo<int>(); | |
| beNull | null.should.beNull(); | |
| beOneOf | 5.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
| Method | Example | Failure message | 
|---|---|---|
| beTrue | false.should.beTrue(); | Target booleanshould be True but was False | 
| beFalse | false.should.not.beFalse(); | Target booleanshould 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
| Method | Example | Failure message | 
|---|---|---|
| bePositive | (-1).should.bePositive(); | Target intshould be positive but was negative as -1 | 
| beNegative | 1.should.beNegative(); | Target intshould be negative but was positive as 1 | 
| beZero | 10.should.beZero(); | Target intshould be 0 but was 10 | 
| beOdd | 8.should.beOdd(); | Target intshould be odd but was even as 8 | 
| beEven | 7.should.beEven(); | Target intshould be event but was odd as 7 | 
| beGreaterThan | 1.should.beGreaterThan(2); | Target intshould be greater than 2 but does not | 
| beAbove | 3.should.beAbove(2); | |
| beLessThan | 3.should.beLessThan(4); | |
| beBelow | 3.should.beBelow(4); | |
| beGreaterOrEqualThan | 3.should.beGreaterOrEqualThan(3); | |
| beLessOrEqualThan | 3.should.beLessOrEqualThan(3); | |
| beWithin | 3.should.beWithin(1,5); | |
| beCloseTo | pi.should.beCloseTo(3.14, delta: 0.01); | |
| beTolerantOf | pi.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
| Method | Example | Failure 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
| Method | Example | Failure message | 
|---|---|---|
| beCloseTo | DateTime.now().should.beCloseTo(Date(2025, 1, 1)); | |
| beAfter | DateTime.now().should.beCloseTo(Date(2022, 1, 1)); | |
| beBefore | DateTime.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
| Method | Example | Failure 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
| Method | Example | Failure 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
| Method | Example | Failure message | 
|---|---|---|
| throwException | Should.throwException(() => myFunc()); | |
| notThrowException | Should.notThrowException(() => myFunc()); | |
| throwError | Should.throwError(() => myFunc()); | |
| notThrowError | Should.notThrowError(() => myFunc()); | |
| completeIn | Should.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.
 
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