diff --git a/lib/src/declarer.dart b/lib/src/declarer.dart new file mode 100644 index 0000000000000000000000000000000000000000..9b3000645068d5cc7b9612708b2071041ceab8c7 --- /dev/null +++ b/lib/src/declarer.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library unittest.declarer; + +import 'dart:collection'; + +import 'group.dart'; +import 'invoker.dart'; +import 'test.dart'; + +/// A class that manages the state of tests as they're declared. +/// +/// This is in charge of tracking the current group, set-up, and tear-down +/// functions. It produces a list of runnable [tests]. +class Declarer { + /// The current group. + var _group = new Group.root(); + + /// The list of tests that have been defined. + List<Test> get tests => new UnmodifiableListView<Test>(_tests); + final _tests = new List<Test>(); + + Declarer(); + + /// Defines a test case with the given description and body. + /// + /// The description will be added to the descriptions of any surrounding + /// [group]s. + void test(String description, body()) { + // TODO(nweiz): Once tests have begun running, throw an error if [test] is + // called. + var prefix = _group.description; + if (prefix != null) description = "$prefix $description"; + + var group = _group; + _tests.add(new LocalTest(description, () { + // TODO(nweiz): It might be useful to throw an error here if a test starts + // running while other tests from the same declarer are also running, + // since they might share closurized state. + return group.runSetUp().then((_) => body()); + }, tearDown: group.runTearDown)); + } + + /// Creates a group of tests. + /// + /// A group's description is included in the descriptions of any tests or + /// sub-groups it contains. [setUp] and [tearDown] are also scoped to the + /// containing group. + void group(String description, void body()) { + var oldGroup = _group; + _group = new Group(oldGroup, description); + try { + body(); + } finally { + _group = oldGroup; + } + } + + /// Registers a function to be run before tests. + /// + /// This function will be called before each test is run. [callback] may be + /// asynchronous; if so, it must return a [Future]. + /// + /// If this is called within a [group], it applies only to tests in that + /// group. [callback] will be run after any set-up callbacks in parent groups + /// or at the top level. + void setUp(callback()) { + if (_group.setUp != null) { + throw new StateError("setUp() may not be called multiple times for the " + "same group."); + } + + _group.setUp = callback; + } + + /// Registers a function to be run after tests. + /// + /// This function will be called after each test is run. [callback] may be + /// asynchronous; if so, it must return a [Future]. + /// + /// If this is called within a [group], it applies only to tests in that + /// group. [callback] will be run before any tear-down callbacks in parent + /// groups or at the top level. + void tearDown(callback()) { + if (_group.tearDown != null) { + throw new StateError("tearDown() may not be called multiple times for " + "the same group."); + } + + _group.tearDown = callback; + } +} diff --git a/lib/src/group.dart b/lib/src/group.dart new file mode 100644 index 0000000000000000000000000000000000000000..e7de57c9d55cc4066982ea6fd0ffe3e75db4217f --- /dev/null +++ b/lib/src/group.dart @@ -0,0 +1,76 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library unittest.group; + +import 'dart:async'; + +import 'utils.dart'; + +/// A group contains multiple tests and subgroups. +/// +/// A group has a description that is prepended to that of all nested tests and +/// subgroups. It also has [setUp] and [tearDown] functions which are scoped to +/// the tests and groups it contains. +class Group { + /// The parent group, or `null` if this is the root group. + final Group parent; + + /// The description of the current test group, or `null` if this is the root + /// group. + final String _description; + + /// The set-up function for this group, or `null`. + AsyncFunction setUp; + + /// The tear-down function for this group, or `null`. + AsyncFunction tearDown; + + /// Returns the description for this group, including the description of any + /// parent groups. + /// + /// If this is the root group, returns `null`. + String get description { + if (parent == null || parent.description == null) return _description; + return "${parent.description} $_description"; + } + + /// Creates a new root group. + /// + /// This is the implicit group that exists outside of any calls to `group()`. + Group.root() + : this(null, null); + + Group(this.parent, this._description); + + /// Run the set-up functions for this and any parent groups. + /// + /// If no set-up functions are declared, this returns a [Future] that + /// completes immediately. + Future runSetUp() { + if (parent != null) { + return parent.runSetUp().then((_) { + if (setUp != null) return setUp(); + }); + } + + if (setUp != null) return new Future.sync(setUp); + return new Future.value(); + } + + /// Run the tear-up functions for this and any parent groups. + /// + /// If no set-up functions are declared, this returns a [Future] that + /// completes immediately. + Future runTearDown() { + if (parent != null) { + return new Future.sync(() { + if (tearDown != null) return tearDown(); + }).then((_) => parent.runTearDown()); + } + + if (tearDown != null) return new Future.sync(tearDown); + return new Future.value(); + } +} diff --git a/test/declarer_test.dart b/test/declarer_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..13211be29b80b3c086f79557d94bde161f7239bb --- /dev/null +++ b/test/declarer_test.dart @@ -0,0 +1,326 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:unittest/src/declarer.dart'; +import 'package:unittest/src/suite.dart'; +import 'package:unittest/unittest.dart'; + +Declarer _declarer; +Suite _suite; + +void main() { + setUp(() { + _declarer = new Declarer(); + _suite = new Suite("suite", []); + }); + + group(".test()", () { + test("declares a test with a description and body", () { + var bodyRun = false; + _declarer.test("description", () { + bodyRun = true; + }); + + expect(_declarer.tests, hasLength(1)); + expect(_declarer.tests.single.name, equals("description")); + + return _runTest(0).then(expectAsync((_) { + expect(bodyRun, isTrue); + }, max: 1)); + }); + + test("declares multiple tests", () { + _declarer.test("description 1", () {}); + _declarer.test("description 2", () {}); + _declarer.test("description 3", () {}); + + expect(_declarer.tests, hasLength(3)); + expect(_declarer.tests[0].name, equals("description 1")); + expect(_declarer.tests[1].name, equals("description 2")); + expect(_declarer.tests[2].name, equals("description 3")); + }); + }); + + group(".setUp()", () { + test("is run before all tests", () { + var setUpRun = false; + _declarer.setUp(() => setUpRun = true); + + _declarer.test("description 1", expectAsync(() { + expect(setUpRun, isTrue); + setUpRun = false; + }, max: 1)); + + _declarer.test("description 2", expectAsync(() { + expect(setUpRun, isTrue); + setUpRun = false; + }, max: 1)); + + return _runTest(0).then((_) => _runTest(1)); + }); + + test("can return a Future", () { + var setUpRun = false; + _declarer.setUp(() { + return new Future(() => setUpRun = true); + }); + + _declarer.test("description", expectAsync(() { + expect(setUpRun, isTrue); + }, max: 1)); + + return _runTest(0); + }); + + test("can't be called multiple times", () { + _declarer.setUp(() {}); + expect(() => _declarer.setUp(() {}), throwsStateError); + }); + }); + + group(".tearDown()", () { + test("is run after all tests", () { + var tearDownRun; + _declarer.setUp(() => tearDownRun = false); + _declarer.tearDown(() => tearDownRun = true); + + _declarer.test("description 1", expectAsync(() { + expect(tearDownRun, isFalse); + }, max: 1)); + + _declarer.test("description 2", expectAsync(() { + expect(tearDownRun, isFalse); + }, max: 1)); + + return _runTest(0).then((_) { + expect(tearDownRun, isTrue); + return _runTest(1); + }).then((_) => expect(tearDownRun, isTrue)); + }); + + test("can return a Future", () { + var tearDownRun = false; + _declarer.tearDown(() { + return new Future(() => tearDownRun = true); + }); + + _declarer.test("description", expectAsync(() { + expect(tearDownRun, isFalse); + }, max: 1)); + + return _runTest(0).then((_) => expect(tearDownRun, isTrue)); + }); + + test("can't be called multiple times", () { + _declarer.tearDown(() {}); + expect(() => _declarer.tearDown(() {}), throwsStateError); + }); + }); + + group("in a group,", () { + test("tests inherit the group's description", () { + _declarer.group("group", () { + _declarer.test("description", () {}); + }); + + expect(_declarer.tests, hasLength(1)); + expect(_declarer.tests.single.name, "group description"); + }); + + group(".setUp()", () { + test("is scoped to the group", () { + var setUpRun = false; + _declarer.group("group", () { + _declarer.setUp(() => setUpRun = true); + + _declarer.test("description 1", expectAsync(() { + expect(setUpRun, isTrue); + setUpRun = false; + }, max: 1)); + }); + + _declarer.test("description 2", expectAsync(() { + expect(setUpRun, isFalse); + setUpRun = false; + }, max: 1)); + + return _runTest(0).then((_) => _runTest(1)); + }); + + test("runs from the outside in", () { + var outerSetUpRun = false; + var middleSetUpRun = false; + var innerSetUpRun = false; + _declarer.setUp(expectAsync(() { + expect(middleSetUpRun, isFalse); + expect(innerSetUpRun, isFalse); + outerSetUpRun = true; + }, max: 1)); + + _declarer.group("middle", () { + _declarer.setUp(expectAsync(() { + expect(outerSetUpRun, isTrue); + expect(innerSetUpRun, isFalse); + middleSetUpRun = true; + }, max: 1)); + + _declarer.group("inner", () { + _declarer.setUp(expectAsync(() { + expect(outerSetUpRun, isTrue); + expect(middleSetUpRun, isTrue); + innerSetUpRun = true; + }, max: 1)); + + _declarer.test("description", expectAsync(() { + expect(outerSetUpRun, isTrue); + expect(middleSetUpRun, isTrue); + expect(innerSetUpRun, isTrue); + }, max: 1)); + }); + }); + + return _runTest(0); + }); + + test("handles Futures when chained", () { + var outerSetUpRun = false; + var innerSetUpRun = false; + _declarer.setUp(expectAsync(() { + expect(innerSetUpRun, isFalse); + return new Future(() => outerSetUpRun = true); + }, max: 1)); + + _declarer.group("inner", () { + _declarer.setUp(expectAsync(() { + expect(outerSetUpRun, isTrue); + return new Future(() => innerSetUpRun = true); + }, max: 1)); + + _declarer.test("description", expectAsync(() { + expect(outerSetUpRun, isTrue); + expect(innerSetUpRun, isTrue); + }, max: 1)); + }); + + return _runTest(0); + }); + + test("can't be called multiple times", () { + _declarer.group("group", () { + _declarer.setUp(() {}); + expect(() => _declarer.setUp(() {}), throwsStateError); + }); + }); + }); + + group(".tearDown()", () { + test("is scoped to the group", () { + var tearDownRun; + _declarer.setUp(() => tearDownRun = false); + + _declarer.group("group", () { + _declarer.tearDown(() => tearDownRun = true); + + _declarer.test("description 1", expectAsync(() { + expect(tearDownRun, isFalse); + }, max: 1)); + }); + + _declarer.test("description 2", expectAsync(() { + expect(tearDownRun, isFalse); + }, max: 1)); + + return _runTest(0).then((_) { + expect(tearDownRun, isTrue); + return _runTest(1); + }).then((_) => expect(tearDownRun, isFalse)); + }); + + test("runs from the inside out", () { + var innerTearDownRun = false; + var middleTearDownRun = false; + var outerTearDownRun = false; + _declarer.tearDown(expectAsync(() { + expect(innerTearDownRun, isTrue); + expect(middleTearDownRun, isTrue); + outerTearDownRun = true; + }, max: 1)); + + _declarer.group("middle", () { + _declarer.tearDown(expectAsync(() { + expect(innerTearDownRun, isTrue); + expect(outerTearDownRun, isFalse); + middleTearDownRun = true; + }, max: 1)); + + _declarer.group("inner", () { + _declarer.tearDown(expectAsync(() { + expect(outerTearDownRun, isFalse); + expect(middleTearDownRun, isFalse); + innerTearDownRun = true; + }, max: 1)); + + _declarer.test("description", expectAsync(() { + expect(outerTearDownRun, isFalse); + expect(middleTearDownRun, isFalse); + expect(innerTearDownRun, isFalse); + }, max: 1)); + }); + }); + + return _runTest(0).then((_) { + expect(innerTearDownRun, isTrue); + expect(middleTearDownRun, isTrue); + expect(outerTearDownRun, isTrue); + }); + }); + + test("handles Futures when chained", () { + var outerTearDownRun = false; + var innerTearDownRun = false; + _declarer.tearDown(expectAsync(() { + expect(innerTearDownRun, isTrue); + return new Future(() => outerTearDownRun = true); + }, max: 1)); + + _declarer.group("inner", () { + _declarer.tearDown(expectAsync(() { + expect(outerTearDownRun, isFalse); + return new Future(() => innerTearDownRun = true); + }, max: 1)); + + _declarer.test("description", expectAsync(() { + expect(outerTearDownRun, isFalse); + expect(innerTearDownRun, isFalse); + }, max: 1)); + }); + + return _runTest(0).then((_) { + expect(innerTearDownRun, isTrue); + expect(outerTearDownRun, isTrue); + }); + }); + + test("can't be called multiple times", () { + _declarer.group("group", () { + _declarer.tearDown(() {}); + expect(() => _declarer.tearDown(() {}), throwsStateError); + }); + }); + }); + }); +} + +/// Runs the test at [index] defined on [_declarer]. +/// +/// This automatically sets up an `onError` listener to ensure that the test +/// doesn't throw any invisible exceptions. +Future _runTest(int index) { + var liveTest = _declarer.tests[index].load(_suite); + liveTest.onError.listen(expectAsync((_) {}, + count: 0, reason: "No errors expected for test #$index.")); + return liveTest.run(); +}