given_when_then_unit_test

pub package likes codecov style: lint Dart

A Flutter package for creating more readable tests. If you are not familiar with Flutter's Unit tests

Given we feel that our tests are the best documentation of the behaviors in our code.
When we read our tests.
Then we want them to be easy to understand.
And test code be elegant.
But be written without any pain.

Features

Improve test code readability

// Without `given_when_then`
group('calculator', () {
  // ...
  group('add 1', () => calc.add(1), then: () {
    test('result should be 1', () {
      // ...
    });

    group('[and] subtract 1', () => calc.subtract(1), body: () {
      test('res should be 0', () {
        // ...
      });
    });
  });
});

// 🔥 With `given_when_then` as a common English sentence
given('calculator', () {
  // ...
  when('add 1', () => calc.add(1), then: () {
    then('result should be 1', () {
      // ...
    });

    when('[and] subtract 1', () => calc.subtract(1), body: () {
      then('res should be 0', () {
        // ...
      });
    });
  });
});

With shouldly it makes super readable test code 😍

given('calculator', () {
  late Calculator calc;

  before(() {
    calc = Calculator();
  });

  when('add 1', () {
    before(() => calc.add(1));

    then('result should be 1', () {
      calc.res.should.be(1);
    });

    when('[and] subtract 1', () {
      before(() => calc.subtract(1));
      then('res should be 0', () {
        calc.res.should.beZero();
      });
    });
  });
});

Auto compose test message as BDD style

Without given_when_then

✓ calculator When add 1 result should be 1
✓ calculator When add 1 [and] subtract 1 res should be 0

With given_when_then with minimal effort

✓ Given empty calculator When add 1 Then result should be 1
✓ Given empty calculator When add 1 and subtract 1 Then res should be 0

Usage

Simple example

Without given_when_then

  group('empty calculator', body: () {
    late Calculator calc;

    setUp(() {
      calc = Calculator();
    });

    group('add 1', () {

      setUp(() {
        calc.add(1);
      });

      test('result should be 1', () {
        calc.res.should.be(1);
      });

      group('[and] subtract 1', () {

        setUp(() {
          calc.subtract(1);
        });

        test('res should be 0', () {
          calc.res.should.beZero();
        });
      });
    });
  });

With given_when_then

  given('empty calculator', () {
    late Calculator calc;

    before(() {
      calc = Calculator();
    });

    when('add 1', () => calc.add(1), then: () {
      then('result should be 1', () {
        calc.res.should.be(1);
      });

      and('subtract 1', () => calc.subtract(1), body: () {
        then('res should be 0', () {
          calc.res.should.beZero();
        });
      });
    });
  });

Advanced example with mocking

given('Post Controller', body: () {
    late PostController postController;
    late IPostRepository mockPostRepository;
    late IToastr mockToastr;

    before(() {
      mockPostRepository = MockPostRepository();
      mockToastr = MockToastr();
      postController = PostController(
        repo: mockPostRepository,
        toastr: mockToastr,
      );
    });

    whenn('save new valid post', () {
      bool? saveResult;

      before(() async {
        when(() => mockPostRepository.addNew('new post'))
            .thenAnswer((_) => Future.value(true));

        saveResult = await postController.addNew('new post');
      });

      then('should return true', () async {
        saveResult.should.beTrue();
      });

      then('toastr shows success', () async {
        verify(() => mockToastr.success('ok')).called(1);
      });
    });

    whenn('save new invalid post', () {
      bool? saveResult;
      before(() async {
        when(() => mockPostRepository.addNew('new invalid post'))
            .thenAnswer((_) => Future.value(false));

        saveResult = await postController.addNew('new invalid post');
      });

      then('should return false', () async {
        saveResult.should.beFalse();
      });

      then('toastr shows error', () async {
        verify(() => mockToastr.error('invalid post')).called(1);
      });
    });
  });

Test cases

There are two ways how to use test cases:

version 1

testCases([
  const TestCase([1, 1, 2]),
  const TestCase([5, 3, 8])
], (testCase) {
  final x = testCase.args[0] as int;
  final y = testCase.args[1] as int;

  given('two numbers $x and $y', () {
    //
    when('summarizing them', () {
      then('the result should be equal to ${testCase.args.last}', () {
        (x + y).should.be(testCase.args[2] as int);
      });
    });
  });
});
✓ 
Given two numbers 1 and 1 
When summarizing them 
Then the result should be equal to 2
✓ 
Given two numbers 5 and 3 
When summarizing them 
Then the result should be equal to 8

version 2 - with generic

testCases2<String, String>([
  const TestCase2('Flutter', 'F'),
  const TestCase2('Awesome', 'A'),
], (args) {
  test('Word ${args.arg1} start with ${args.arg2}', () {
    args.arg1.should.startWith(args.arg2);
  });
});
✓ Word Flutter start with F
✓ Word Awesome start with A

Formatting the test report 📜

You can format the test report, make it in a single line, or print every step on each line by setting variable GivenWhenThenOptions.pads with any integer value, e.g.

GivenWhenThenOptions.pads = 4;

and result will be:

Given the account balance is $100 
    And the card is valid 
    And the machine contains enough money 
When the Account Holder requests $20 
Then the Cashpoint should dispense
    And the account balance should be $80
    And the card should be returned

Known Issues

  • Collision with mocktail or mockito packages which bring where method too, you can hide when and use whenn of this package like below

But prefer to hide and rename imports like so.

import 'package:mocktail/mocktail.dart' hide when;
import 'package:mocktail/mocktail.dart' as mktl show when;
import 'package:given_when_then_unit_test/given_when_then_unit_test.dart' hide when;

void main() {
  given('Post Controller', () {
    // .. omit
    whenn('save new invalid post', () {
      // ... omit
      then('should return false', () async {
        saveResult.should.beFalse();
      });

      then('toastr shows error', () async {
        verify(() => mockToastr.error('invalid post')).called(1);
      });
    });
  });
}

And & But

Steps And and But are inter-changeable.

However, But in the English language is generally used in a negative context. And using But makes the intent of the test explicit and removes any possible ambiguities.

Contributing

We accept the following contributions:

  • Ideas how to improve readability or performance
  • Reporting issues
  • Fixing bugs
  • Improving documentation and comments

Maintainers