An exploration into test driven development with dependency injection.
- You will learn how to write unit test code using test driven development.
- You will learn how to test code that deals with dates and time.
In industry you want to test your business logic, and that's hard if everything in your code is referencing dates.
By using dependency injection, with the help of a time traveler, you'll learn how to write more easily tested code, so that you don't have to wait days to know if code is right.
Test driven development follows design and exploration. If you don't know how to use the APIs, you need to explore before you can design the high level direction.
I've already done that work for this example, you'll be building a starting point for trial period logic. That you can leverage in your app to ask a user to purchase, or to inform them when the trial expires.
let trial = TrialPeriod()
if trial.isExpired() {
showPurchaseDialog()
} else {
showTrialDialogWithExpiration(trial.dateExpired)
}
This system needs to save and load data to disk, so that when the app is closed, it can still remember when the app was first installed. That's beyond the scope of this talk, but you can extend it afterwards.
class TrialPeriod {
var dateInstalled: Date
var durationInDays: Int
var dateExpired: Date { get }
func isExpired() -> Bool
func resetTrial()
}
In Xcode you want to run unit tests fast, and by default, unit tests have to start the app, which allows you to test UIKit related logic.
You can disable this behavior, by editing your Unit Test target's General properties.
- Project Properties > General > Targets > TestingTimeTDD > General
- Set "Host Application" to None
NOTE: All source files must be added to the Unit Test Target
Uncle Bob's 3 Laws of TDD
- You must write a failing test before you write any production code.
- You must not write more of a test than is sufficient to fail, or fail to compile.
- You must not write more production code than is sufficient to make the currently failing test pass.
- Red: Create a unit tests that fails.
- Green: Write production code that makes that test pass.
- Refactor: Clean up the mess you just made.
You should only have one test failing at a time, and you should not refactor when tests are failing.
If one of your tests cannot pass because you need to refactor, comment out the test, cleanup the logic, make sure the previous tests pass, and then make changes to fix the new test pass.
Generally your tests should have one assertion, however for clarity you may want to add more to help the reader understand what's being tested.
func testArrangeActAssert() {
// Arrange
let x = 20
let y = 40
let expected = 60
// Act
let actual = x + y
// Assert
XCTAssertEqual(expected, actual)
}
As you refactor, you can simply tests down to less lines of code by leveraging the setUp()
method.
Using this model as a guide helps you structure tests that are easy to read.
func testTrialInstallDateIsSetOnStart()
func testDurationInDaysIs7()
func testExpiredDate()
func testTrialNotExpiredAfter0Days()
func testTrialNotExpiredAfter7Days()
func testTrialExpiresAfter21Days()
func testResetChangesDateToCurrentTime()