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
should
property 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
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:
Method | Example | Failure message |
---|---|---|
be | 1.should.be(2); |
Expected int should 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 boolean should be True but was False |
beFalse | false.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
Method | Example | Failure message |
---|---|---|
bePositive | (-1).should.bePositive(); |
Target int should be positive but was negative as -1 |
beNegative | 1.should.beNegative(); |
Target int should be negative but was positive as 1 |
beZero | 10.should.beZero(); |
Target int should be 0 but was 10 |
beOdd | 8.should.beOdd(); |
Target int should be odd but was even as 8 |
beEven | 7.should.beEven(); |
Target int should be event but was odd as 7 |
beGreaterThan | 1.should.beGreaterThan(2); |
Target int should 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