Permalink
120e3b1 Dec 26, 2017
2 contributors

Users who have contributed to this file

@modocache @freak4pc
492 lines (372 sloc) 11.6 KB

Organized Tests with Quick Examples and Example Groups

Quick uses a special syntax to define examples and example groups.

In Effective Tests Using XCTest: Arrange, Act, and Assert, we learned that a good test method name is crucial--when a test starts failing, it's the best way to determine whether we have to fix the application code or update the test.

Quick examples and example groups serve two purposes:

  1. They encourage you to write descriptive test names.
  2. They greatly simplify the test code in the "arrange" step of your tests.

Examples Using it

Examples, defined with the it function, use assertions to demonstrate how code should behave. These are like test methods in XCTest.

it takes two parameters: the name of the example, and a closure. The examples below specify how the Sea.Dolphin class should behave. A new dolphin should be smart and friendly:

// Swift

import Quick
import Nimble
import Sea

class DolphinSpec: QuickSpec {
  override func spec() {
    it("is friendly") {
      expect(Dolphin().isFriendly).to(beTruthy())
    }

    it("is smart") {
      expect(Dolphin().isSmart).to(beTruthy())
    }
  }
}
// Objective-C

@import Quick;
@import Nimble;

QuickSpecBegin(DolphinSpec)

it(@"is friendly", ^{
  expect(@([[Dolphin new] isFriendly])).to(beTruthy());
});

it(@"is smart", ^{
  expect(@([[Dolphin new] isSmart])).to(beTruthy());
});

QuickSpecEnd

Use descriptions to make it clear what your examples are testing. Descriptions can be of any length and use any character, including characters from languages besides English, or even emoji! ✌️ 😎

Example Groups Using describe and context

Example groups are logical groupings of examples. Example groups can share setup and teardown code.

Describing Classes and Methods Using describe

To specify the behavior of the Dolphin class's click method--in other words, to test the method works--several it examples can be grouped together using the describe function. Grouping similar examples together makes the spec easier to read:

// Swift

import Quick
import Nimble

class DolphinSpec: QuickSpec {
  override func spec() {
    describe("a dolphin") {
      describe("its click") {
        it("is loud") {
          let click = Dolphin().click()
          expect(click.isLoud).to(beTruthy())
        }

        it("has a high frequency") {
          let click = Dolphin().click()
          expect(click.hasHighFrequency).to(beTruthy())
        }
      }
    }
  }
}
// Objective-C

@import Quick;
@import Nimble;

QuickSpecBegin(DolphinSpec)

describe(@"a dolphin", ^{
  describe(@"its click", ^{
    it(@"is loud", ^{
      Click *click = [[Dolphin new] click];
      expect(@(click.isLoud)).to(beTruthy());
    });

    it(@"has a high frequency", ^{
      Click *click = [[Dolphin new] click];
      expect(@(click.hasHighFrequency)).to(beTruthy());
    });
  });
});

QuickSpecEnd

When these two examples are run in Xcode, they'll display the description from the describe and it functions:

  1. DolphinSpec.a_dolphin_its_click_is_loud
  2. DolphinSpec.a_dolphin_its_click_has_a_high_frequency

Again, it's clear what each of these examples is testing.

Sharing Setup/Teardown Code Using beforeEach and afterEach

Example groups don't just make the examples clearer, they're also useful for sharing setup and teardown code among examples in a group.

In the example below, the beforeEach function is used to create a brand new instance of a dolphin and its click before each example in the group. This ensures that both are in a "fresh" state for every example:

// Swift

import Quick
import Nimble

class DolphinSpec: QuickSpec {
  override func spec() {
    describe("a dolphin") {
      var dolphin: Dolphin!
      beforeEach {
        dolphin = Dolphin()
      }

      describe("its click") {
        var click: Click!
        beforeEach {
          click = dolphin.click()
        }

        it("is loud") {
          expect(click.isLoud).to(beTruthy())
        }

        it("has a high frequency") {
          expect(click.hasHighFrequency).to(beTruthy())
        }
      }
    }
  }
}
// Objective-C

@import Quick;
@import Nimble;

QuickSpecBegin(DolphinSpec)

describe(@"a dolphin", ^{
  __block Dolphin *dolphin = nil;
  beforeEach(^{
      dolphin = [Dolphin new];
  });

  describe(@"its click", ^{
    __block Click *click = nil;
    beforeEach(^{
      click = [dolphin click];
    });

    it(@"is loud", ^{
      expect(@(click.isLoud)).to(beTruthy());
    });

    it(@"has a high frequency", ^{
      expect(@(click.hasHighFrequency)).to(beTruthy());
    });
  });
});

QuickSpecEnd

Sharing setup like this might not seem like a big deal with the dolphin example, but for more complicated objects, it saves a lot of typing!

To execute code after each example, use afterEach.

Specifying Conditional Behavior Using context

Dolphins use clicks for echolocation. When they approach something particularly interesting to them, they release a series of clicks in order to get a better idea of what it is.

The tests need to show that the click method behaves differently in different circumstances. Normally, the dolphin just clicks once. But when the dolphin is close to something interesting, it clicks several times.

This can be expressed using context functions: one context for the normal case, and one context for when the dolphin is close to something interesting:

// Swift

import Quick
import Nimble

class DolphinSpec: QuickSpec {
  override func spec() {
    describe("a dolphin") {
      var dolphin: Dolphin!
      beforeEach { dolphin = Dolphin() }

      describe("its click") {
        context("when the dolphin is not near anything interesting") {
          it("is only emitted once") {
            expect(dolphin.click().count).to(equal(1))
          }
        }

        context("when the dolphin is near something interesting") {
          beforeEach {
            let ship = SunkenShip()
            Jamaica.dolphinCove.add(ship)
            Jamaica.dolphinCove.add(dolphin)
          }

          it("is emitted three times") {
            expect(dolphin.click().count).to(equal(3))
          }
        }
      }
    }
  }
}
// Objective-C

@import Quick;
@import Nimble;

QuickSpecBegin(DolphinSpec)

describe(@"a dolphin", ^{
  __block Dolphin *dolphin = nil;
  beforeEach(^{ dolphin = [Dolphin new]; });

  describe(@"its click", ^{
    context(@"when the dolphin is not near anything interesting", ^{
      it(@"is only emitted once", ^{
        expect(@([[dolphin click] count])).to(equal(@1));
      });
    });

    context(@"when the dolphin is near something interesting", ^{
      beforeEach(^{
        [[Jamaica dolphinCove] add:[SunkenShip new]];
        [[Jamaica dolphinCove] add:dolphin];
      });

      it(@"is emitted three times", ^{
        expect(@([[dolphin click] count])).to(equal(@3));
      });
    });
  });
});

QuickSpecEnd

Strictly speaking, the context keyword is a synonym for describe, but thoughtful use will make your spec easier to understand.

Test Readability: Quick and XCTest

In Effective Tests Using XCTest: Arrange, Act, and Assert, we looked at how one test per condition was a great way to organize test code. In XCTest, that leads to long test method names:

func testDolphin_click_whenTheDolphinIsNearSomethingInteresting_isEmittedThreeTimes() {
  // ...
}

Using Quick, the conditions are much easier to read, and we can perform setup for each example group:

describe("a dolphin") {
  describe("its click") {
    context("when the dolphin is near something interesting") {
      it("is emitted three times") {
        // ...
      }
    }
  }
}

Temporarily Disabling Examples or Groups

You can temporarily disable examples or example groups that don't pass yet. The names of the examples will be printed out along with the test results, but they won't be run.

You can disable an example or group by prepending x:

// Swift

xdescribe("its click") {
  // ...none of the code in this closure will be run.
}

xcontext("when the dolphin is not near anything interesting") {
  // ...none of the code in this closure will be run.
}

xit("is only emitted once") {
  // ...none of the code in this closure will be run.
}
// Objective-C

xdescribe(@"its click", ^{
  // ...none of the code in this closure will be run.
});

xcontext(@"when the dolphin is not near anything interesting", ^{
  // ...none of the code in this closure will be run.
});

xit(@"is only emitted once", ^{
  // ...none of the code in this closure will be run.
});

Temporarily Running a Subset of Focused Examples

Sometimes it helps to focus on only one or a few examples. Running one or two examples is faster than the entire suite, after all. You can run only one or two by using the fit function. You can also focus a group of examples using fdescribe or fcontext:

fit("is loud") {
  // ...only this focused example will be run.
}

it("has a high frequency") {
  // ...this example is not focused, and will not be run.
}

fcontext("when the dolphin is near something interesting") {
  // ...examples in this group are also focused, so they'll be run.
}
fit(@"is loud", {
  // ...only this focused example will be run.
});

it(@"has a high frequency", ^{
  // ...this example is not focused, and will not be run.
});

fcontext(@"when the dolphin is near something interesting", ^{
  // ...examples in this group are also focused, so they'll be run.
});

Global Setup/Teardown Using beforeSuite and afterSuite

Some test setup needs to be performed before any examples are run. For these cases, use beforeSuite and afterSuite.

In the example below, a database of all the creatures in the ocean is created before any examples are run. That database is torn down once all the examples have finished:

// Swift

import Quick

class DolphinSpec: QuickSpec {
  override func spec() {
    beforeSuite {
      OceanDatabase.createDatabase(name: "test.db")
      OceanDatabase.connectToDatabase(name: "test.db")
    }

    afterSuite {
      OceanDatabase.teardownDatabase(name: "test.db")
    }

    describe("a dolphin") {
      // ...
    }
  }
}
// Objective-C

@import Quick;

QuickSpecBegin(DolphinSpec)

beforeSuite(^{
  [OceanDatabase createDatabase:@"test.db"];
  [OceanDatabase connectToDatabase:@"test.db"];
});

afterSuite(^{
  [OceanDatabase teardownDatabase:@"test.db"];
});

describe(@"a dolphin", ^{
  // ...
});

QuickSpecEnd

You can specify as many beforeSuite and afterSuite as you like. All beforeSuite closures will be executed before any tests run, and all afterSuite closures will be executed after all the tests are finished. There is no guarantee as to what order these closures will be executed in.

Accessing Metadata for the Current Example

There may be some cases in which you'd like the know the name of the example that is currently being run, or how many have been run so far. Quick provides access to this metadata in beforeEach and afterEach closures.

beforeEach { exampleMetadata in
  println("Example number \(exampleMetadata.exampleIndex) is about to be run.")
}

afterEach { exampleMetadata in
  println("Example number \(exampleMetadata.exampleIndex) has run.")
}
beforeEachWithMetadata(^(ExampleMetadata *exampleMetadata){
  NSLog(@"Example number %l is about to be run.", (long)exampleMetadata.exampleIndex);
});

afterEachWithMetadata(^(ExampleMetadata *exampleMetadata){
  NSLog(@"Example number %l has run.", (long)exampleMetadata.exampleIndex);
});