FizzBuzzThrow Kata
FizzBuzz Throw is a variation of the famous FizzBuzz Kata and specifically aims to focus on working with Errors.
The rules are the following:
Write a function that accepts a number and throws an Error:
a. “fizzError” if the number is divisible by 3
b. “buzzError” if the number is divisible by 5
c. “fizzBuzzError” if the number is divisible by 3 and 5
Otherwise, it returns the input number.
Let’s start the step by step solution.
Let’s start by creating a new project. Open the Xcode and select File > New > iOS > Framework and name it FizzBuzzThrowCalculator. As in the previous example, let’s delete all unnecessary code:
1 2 3 4 5 6 |
import XCTest @testable import FizzBuzzThrowCalculator class FizzBuzzThrowCalculatorTests: XCTestCase { } |
Now let’s tackle the simplest requirement: “FizzBuzzThrow returns the input number if the number is not divisible by three or five”. This requirement’s implementation is very similar, with the equivalent requirement of the classic FizzBuzz challenge, so we will omit the explanation here.
The tests as previously are:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import XCTest @testable import FizzBuzzThrowCalculator class FizzBuzzThrowCalculatorTests: XCTestCase { func test_fizzBuzz_whenInputIs1_returns1() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() let result = fizzBuzzCalculator.fizzBuzz(number: 1) XCTAssertEqual("1", result) } func test_fizzBuzz_whenInputIs2_returns2() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() let result = fizzBuzzCalculator.fizzBuzz(number: 2) XCTAssertEqual("2", result) } func test_fizzBuzz_whenInputIs4_returns4() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() let result = fizzBuzzCalculator.fizzBuzz(number: 4) XCTAssertEqual("4", result) } } |
And the implementation:
1 2 3 4 5 6 7 8 |
import Foundation class FizzBuzzThrowCalculator { func fizzBuzz(number: Int) -> String { return "\(number)" } } |
The next simplest requirement is “FizzBuzz throws fizzError if the number is divisible by 3”. In our test, we will need to find a way to catch the error and then check that the error is the expected one. XCTest provides us with the function XCTAssertThrowsError, which we can use to catch the error.
1 2 3 4 5 6 7 8 9 10 |
func test_fizzBuzz_whenInputIs3_throwsFizzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() var thrownError: Error? XCTAssertThrowsError(try fizzBuzzCalculator.fizzBuzz(number: 3)) { thrownError = $0 } XCTAssertEqual(FizzBuzzThrowCalculator.FizzBuzzError.fizzError, thrownError as? FizzBuzzThrowCalculator.FizzBuzzError) } |
Trying to compile it, we get the error: “’FizzBuzzError’ is not a member type of ‘FizzBuzzThrowCalculator’”. Let’s create the FizzBuzzError on our implementation code:
Now we get the warning: “No calls to throwing functions occur within ‘try’ expression”. To fix the warning we have to make the method to throw:
1 2 3 |
func fizzBuzz(number: Int) throws -> String { return "\(number)" } |
Now we notice that our previous tests are failing. We are trying to add a change that breaks the current API. In these cases, it is advisable to comment the new test test_fizzBuzz_whenInoutIs3_throwsFizzError, move the rest of the tests to a green state, and then come back to finish the new test. As this is likely to make the rest compile and pass, this only requires adding the “try?” keyword in front of the method call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class FizzBuzzThrowCalculatorTests: XCTestCase { func test_fizzBuzz_whenInputIs1_returns1() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() let result = try? fizzBuzzCalculator.fizzBuzz(number: 1) XCTAssertEqual("1", result) } func test_fizzBuzz_whenInputIs2_returns2() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() let result = try? fizzBuzzCalculator.fizzBuzz(number: 2) XCTAssertEqual("2", result) } func test_fizzBuzz_whenInputIs4_returns4() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() let result = try? fizzBuzzCalculator.fizzBuzz(number: 4) XCTAssertEqual("4", result) } // // func test_fizzBuzz_whenInputIs3_throwsFizzError() { // let fizzBuzzCalculator = FizzBuzzThrowCalculator() // // var thrownError: Error? // // XCTAssertThrowsError(try fizzBuzzCalculator.fizzBuzz(number: 3)) { // thrownError = $0 // } // // XCTAssertEqual(FizzBuzzThrowCalculator.FizzBuzzError.fizzError, thrownError as? FizzBuzzThrowCalculator.FizzBuzzError) // } } |
Being in a green state, we can now uncomment the test and make it pass. All we have todo is throw FizzBuzzError.fizzError when the input number is 3:
1 2 3 4 5 6 |
func fizzBuzz(number: Int) throws -> String { if number == 3 { throw FizzBuzzError.fizzError } return "\(number)" } |
Now let’s try with a multiple of 3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func test_fizzBuzz_whenInputIsMultipleOf3_throwsFizzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() var thrownError: Error? XCTAssertThrowsError(try fizzBuzzCalculator.fizzBuzz(number: anyIntAboveOne() * 3)) { thrownError = $0 } XCTAssertEqual(FizzBuzzThrowCalculator.FizzBuzzError.fizzError, thrownError as? FizzBuzzThrowCalculator.FizzBuzzError) } private func anyIntAboveOne() -> Int { return 2 } |
As in the previous example, the change to make it pass is very simple:
1 2 3 4 5 6 |
func fizzBuzz(number: Int) throws -> String { if number % 3 == 0 { throw FizzBuzzError.fizzError } return "\(number)" } |
Can we refactor? The production code looks very simple, but in the test’s code we notice a lot of duplication on how we catch and assert the error. We can use a helper method and move all the repeated code there. John Sundell (https://www.swiftbysundell.com/posts/testing-error-code-paths-in-swift) has an excellent method for that purpose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
extension XCTestCase { // https://www.swiftbysundell.com/posts/testing-error-code-paths-in-swift func assertThrow<T, E: Error & Equatable>( _ expression: @autoclosure () throws -> T, throws error: E, in file: StaticString = #file, line: UInt = #line ) { var thrownError: Error? XCTAssertThrowsError(try expression(), file: file, line: line) { thrownError = $0 } XCTAssertTrue( thrownError is E, "Unexpected error type: \(type(of: thrownError))", file: file, line: line ) XCTAssertEqual( thrownError as? E, error, file: file, line: line ) } } |
Now we can refactor our tests using this method:
1 2 3 4 5 6 7 8 9 10 11 12 |
//MARK: fizzBuzz - Fizz func test_fizzBuzz_whenInputIs3_throwsFizzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() assertThrow(try fizzBuzzCalculator.fizzBuzz(number: 3), throws: FizzBuzzThrowCalculator.FizzBuzzError.fizzError) } func test_fizzBuzz_whenInputIsMultipleOf3_throwsFizzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() assertThrow(try fizzBuzzCalculator.fizzBuzz(number: anyIntAboveOne() * 3), throws: FizzBuzzThrowCalculator.FizzBuzzError.fizzError) } |
Using helper methods to remove duplication and to increase assertion readability can benefit much of the Test Suite’s maintainability.
Testing error paths and removing duplication on assertions was the tricky part of this challenge. Now we can progress and finish with the rest of the requirements: FizzBuzz throws buzzError if the number is divisible by 5
The tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
The tests: //MARK: fizzBuzz - Buzz func test_fizzBuzz_whenInputIs5_throwsBuzzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() assertThrow(try fizzBuzzCalculator.fizzBuzz(number: 5), throws: FizzBuzzThrowCalculator.FizzBuzzError.buzzError) } func test_fizzBuzz_whenInputIsMultipleOf5_throwsBuzzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() assertThrow(try fizzBuzzCalculator.fizzBuzz(number: anyIntAboveOne() * 5), throws: FizzBuzzThrowCalculator.FizzBuzzError.buzzError) } |
And the implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum FizzBuzzError: Error, Equatable { case fizzError case buzzError } func fizzBuzz(number: Int) throws -> String { if number % 3 == 0 { throw FizzBuzzError.fizzError } else if number % 5 == 0 { throw FizzBuzzError.buzzError } return "\(number)" } |
Of course, we can refactor here to remove the duplication.
1 2 3 4 5 6 7 8 9 10 11 12 |
public func fizzBuzz(number: Int) throws -> String { if isDivisibleBy(numerator: number, denominator: 3) { throw FizzBuzzError.fizzError } else if isDivisibleBy(numerator: number, denominator: 5) { throw FizzBuzzError.buzzError } return "\(number)" } private func isDivisibleBy(numerator: Int, denominator: Int) -> Bool { return numerator % denominator == 0 } |
Finally, let’s implement the last requirement: FizzBuzz throws fizzBuzzError if the number is divisible by 3 and 5.
1 2 3 4 5 6 |
//MARK: fizzBuzz - FizzBuzz func test_fizzBuzz_whenInputIsMultipleOf3And5_throwsFizzBuzzError() { let fizzBuzzCalculator = FizzBuzzThrowCalculator() assertThrow(try fizzBuzzCalculator.fizzBuzz(number: 3 * 5), throws: FizzBuzzThrowCalculator.FizzBuzzError.fizzBuzzError) } |
And the implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import Foundation class FizzBuzzThrowCalculator { enum FizzBuzzError: Error, Equatable { case fizzError case buzzError case fizzBuzzError } func fizzBuzz(number: Int) throws -> String { if isDivisibleBy(numerator: number, denominator: 3) && isDivisibleBy(numerator: number, denominator: 5) { throw FizzBuzzError.fizzBuzzError } else if isDivisibleBy(numerator: number, denominator: 3) { throw FizzBuzzError.fizzError } else if isDivisibleBy(numerator: number, denominator: 5) { throw FizzBuzzError.buzzError } return "\(number)" } func isDivisibleBy(numerator: Int, denominator: Int) -> Bool { return numerator % denominator == 0 } } |