Let’s add a second test for noon, the border where the greeting changes to “Good afternoon.” For step 1 (red), we want a failing test. Duplicate the first test, changing its input, output, and test name:
| func test_greet_with1200pm_shouldSayGoodAfternoon() { |
| let sut = Greeter(name: "") |
| |
| let result = sut.greet(time: date(hour: 12, minute: 00)) |
| |
| XCTAssertEqual(result, "Good afternoon.") |
| } |
Run tests. This new test fails as expected, with this message:
| XCTAssertEqual failed: ("Good morning.") is not equal to ("Good afternoon.") |
On to step 2 (green), getting this test to pass. What’s the simplest thing we can do? We can add a conditional that checks if the hour is 12. To determine the hour of the given time, we can use DateComponents again, but this time in production code:
| func greet(time: Date) -> String { |
» | let components = Calendar.current.dateComponents([.hour], from: time) |
» | if components.hour! == 12 { |
» | return "Good afternoon." |
» | } |
| return "Good morning." |
| } |
The tests pass. Again, we know this code is incomplete. “Good afternoon” will be the greeting for more than when the hour is 12. This tells us that we’ll need another “Good afternoon” test at the far end of the boundary condition.
And this code is inelegant. But remember, we’ll have an opportunity to clean it up soon in step 3. The important thing for step 2 is that all tests pass.
Now on to step 3 (refactor). Let’s start by cleaning up production code. The first nagging thing is where we’re force-unwrapping the hour from the date components. While it should be safe because we asked for .hour components, let’s avoid force-unwrapping in production code. Change this to nil-coalesce, returning 0 if the hour is missing for any reason:
| if components.hour ?? 0 == 12 { |
Run tests to confirm this refactoring.
The next thing we can clean up is to extract the bit that determines the hour. To separate this from the comparison against 12, let’s apply the Extract Variable refactoring. Select components.hour ?? 0 and extract it using Xcode’s automated refactoring if it lets you. Run tests.
Then use the Extract Function refactoring to create and call the following method:
| private func hour(for time: Date) -> Int { |
| let components = Calendar.current.dateComponents([.hour], from: time) |
| return components.hour ?? 0 |
| } |
This makes the code for greet(time:) easier to read. As always, run tests to confirm these changes:
| func greet(time: Date) -> String { |
| if hour(for: time) == 12 { |
| return "Good afternoon." |
| } |
| return "Good morning." |
| } |
Let’s turn our attention to cleaning up the test code. There’s duplication around creating the Greeter, passing it an empty name. But we know that later we’ll have tests where we do pass in a name.
The choice we face is whether to create a new instance in each test or to create separate test suites with different setUp methods. This varies by context and can be a matter of preference. For the sake of example, let’s plan to use separate test suites—so let’s extract the repeated code into a test fixture:
| private var sut: Greeter! |
| |
| override func setUp() { |
| super.setUp() |
| sut = Greeter(name: "") |
| } |
| |
| override func tearDown() { |
| sut = nil |
| super.tearDown() |
| } |
With this fixture in place, delete let sut = Greeter(name: "") from both tests. To make the meaning of this common SUT more obvious, rename the class from GreeterTests to GreeterWithoutNameTests. (Don’t use Xcode’s “Rename” refactoring for this because we want to keep the same file name. Just change the class name by hand.) Run tests to confirm these changes.
This completes our three steps for the new test case of 12:00 p.m.