• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

Test-Driven Development (TDD) in Swift

Share
Tweet
Share
Pin

This article covers TDD, a popular and sometimes controversial approach to writing tests for code. We briefly explain the approach and process that underpins TDD, and touch upon the benefits of TDD before diving into an extended case study to illustrate the process of using TDD in practice. The article shows not only how TDD can help drive development of code in small increments but also how having a comprehensive suite of tests that TDD enables by design makes it easier and safer to refactor code.

Related article: Unit Testing and UI Testing in Swift

Contents

What is TDD
Why TDD
Case study
1. Fees
2. Invoices
3. Courses
4. Enrolled students
5. Casual students
6. Refactoring (round 1)
7. Generating invoices
8. Refactoring (round 2)
Conclusion

What is TDD

TDD is a methodology of using tests to drive development of code. TDD leads to writing test and production code in tandem, using small incremental cycles consisting of three steps:

  1. Red: Write just enough test code until you get a failing test, where failure of the test code to compile is considered a failing test.
  2. Green: Write just enough production code to make the failing test pass.
  3. Refactor: Improve the code by changing its internal structure without altering its external behaviour, and without breaking any tests.

What this means in practice is that we always start by writing test code, specifically a test that is meant to fail because we haven’t implemented the functionality that we are testing. We write production code only in two situations — either to make a failing test pass or to refactor code that passes all existing tests to improve the quality of the code without adding or changing any functionality. Refactoring is carried out in small increments, running the tests after each change to ensure we don’t accidentally break anything. This practice of writing tests before implementing the functionality to be tested, and making sure we see a test fail before writing code to make it pass, may seem a bit strange at first but it plays a critical role in ensuring that test code itself is correct and tests don’t pass for the wrong reasons.

Note that, in practice, every test may not fail when written. Some tests may pass with the existing production code. In such cases, we can modify the production code in specific ways that will make the new test fail, to give us confidence in the correctness of the test. We can then restore the production code to its original state, see the test pass, and continue on our way. We will not show this extra step in this article, in the interest of space, but when using TDD it is customary to see each test fail before making it pass. This ensures that we are sure about the validity of the tests, which instills more faith in the correctness of the code when it passes the tests.

TDD differs from how we think about testing in general. We would normally expect to have the code we are testing written before we test it. TDD is more akin to writing tests as a specification of what we expect the code to do. But, unlike a generic test-first approach where we may write a bunch of tests that we would expect the code to pass before we write the code, in TDD we write tests one micro feature at a time and write production code to pass each test before writing the next one, leading to a continuous cycle of feedback that drives the development of code.

Why TDD

TDD can seem a bit tedious at first, as those new to TDD or unconvinced of its benefits will feel as they start to go through the case study in the next section. Writing test and production code in such small increments, and constantly switching between test and production code, can seem unnecessary. But there are good reasons to follow the rhythm and discipline of TDD.

Listed below are some of the key benefits of TDD:

  1. Interface before implementation: TDD forces us to think about how the code will be used before it is written. We must first put ourselves in the shoes of the programmer who will call our code, which tends to clarify the intent and improve the interface of each unit of code, from individual methods to larger parts of the application. This makes us think about the interface of the code before, and independently of, its implementation.
  2. More flexible and maintainable code: Writing code in a way that makes each unit of code independently testable forces us to decouple units of code from their environment. This tends to make the overall design more flexible, and easier to extend and maintain.
  3. Tests as documentation: Tests written using TDD provide a useful way to document the code. Each test usually verifies a micro feature of the code or validates that the code deals correctly with an edge case. We can understand at a granular level what the code is supposed to do just by going through the tests. In fact, if the tests are named using an expressive and consistent syntax, even glancing through the test names can give a fairly good idea of what the code is supposed to do. This can be especially useful when we are dealing with code written by someone else, or even code that we wrote some time ago.
  4. Easier refactoring: With a suite of tests that validate each micro feature and edge case, we can refactor code with much more confidence, secure in the knowledge that as long as all the tests pass, the code continues to deliver the required functionality. Since refactoring in small increments is part of the rhythm of TDD, and refactoring is done only after the code passes all the tests, we are always one small step away from returning the code to a stable state. Running tests frequently during refactoring can make even major refactoring of the code relatively painless, as we will see when we get into the TDD case study later in this article.
  5. Only required code: We know we are done when we have no further tests to write. This helps write only code that is required to implement the features we need, and handle the expected edge cases. It prevents the tendency to write code that we find may not be required once we write parts of the application that will use the code. It can also reduce instances of having to change the interface of units of code when we have to use them since TDD forces us to think about how we will use a unit of code before we implement it.
  6. Less debugging: TDD by its very nature leads to code that is correct by design. The moment any test fails, whether it is the last test we wrote or one of the existing tests, we stop writing any more test code and modify the production code only to make all tests pass without adding any new feature or functionality. We add new features by adding new tests only once all existing tests pass. This, combined with the fact that TDD imposes a process where we add test and production code one micro feature at a time, makes it easier to identify minor issues before they get buried under layers of code and become hard-to-locate bugs. This can in many cases significantly reduce debugging at a later stage.

In spite of these advantages, TDD is no panacea, and it is by no means the dominant approach to testing. It has fervent supporters and strong detractors in equal measure and, like most coding practices and conventions, it works well when adopted across the team.

Case study

The best way to get our heads around TDD is to see it in action. In the remainder of this article, we will use an extended case study to demonstrate how to write and refactor code using TDD.

Let’s assume we are designing an application to model courses taken, and fees to be paid, by students studying in a college. To keep the example simple, let’s say the college has two types of students – students who are enrolled in the college, and casual students who are not enrolled and may take courses as and when they like. We would like each student instance to keep track of the courses the student is currently taking, and use that to generate an invoice containing fees payables by the student, using a fee per course credit. This applies to both enrolled and casual students although the fee per course credit payable by enrolled and casual students is different. Enrolled students also pay a fixed enrollment fee in addition to the course fees, which should be included in the invoices for enrolled students.

1. Fees

We start by writing tests for a type that will represent a fee. A fee should have an amount which could be either on account of enrollment or for a course with a title. So the requirement we want our code to satisfy is that every fee should have an amount along with a category with two possible values – one representing enrollment and the other representing a course with a title.

Let’s first create a skeleton for a test class, where the system under test, or sut for short, is a variable that can hold an instance of a Fee type. Note that we define sut as an implicitly unwrapped optional, which we set to nil in the tearDown() method. This ensures that each test gets its own instance of Fee, making the tests independent of each other.

As we start writing the test class, we encounter our first compilation error when we define the variable sut to be of type Fee, which we haven’t defined yet.

class FeeTests: XCTestCase {
    private var sut: Fee!
    // ... TO BE CONTINUED
}

We stop writing test code here because we have encountered a failure of the test code to compile, which is regarded in TDD as a failed test. If you are new to TDD, you should get used to encountering compilation errors in test code since this is an essential part of the practice of TDD. We must now write production code to remove this compilation error before writing any more test code. So we define the following type in production code, which is just enough to make the test code compile.

struct Fee: Equatable {
}

Note that we make the Fee type conform to Equatable. This is something we will do for all our value types since attribute-based equality is one of the defining characteristics of values. We will not write any tests for Equatable conformance of value types since it is not a feature being added to a specific value type but rather a defining characteristic of all value types.

This production code removes the compilation error in the test code and allows us to finish writing the skeleton of the test class, as shown below.

class FeeTests: XCTestCase {
    private var sut: Fee!
    
    override func tearDown() {
        sut = nil
    }
}

Now we are ready to add the first test method to this class. This will validate that a Fee instance can be initialized by passing in as the argument a category enumeration with the value enrollment, and that the value passed in through the initializer is assigned to an instance property named category.

Here is how we start writing the test until we encounter a compilation failure because the Fee type does not have a nested enum named Cateogry with a case called enrollment.

func testCategory_hasValueEnrollmentPassedInThroughIntitializer() {
    // given
    let testCategory = Fee.Category.enrollment
    // ... TO BE CONTINUED
}

We cannot write more test code until we have removed this compilation failure. Here is the simplest production code that will make our test code compile.

struct Fee: Equatable {
    enum Category: Equatable {
        case enrollment
    }
}

Moving along, we add more code to the above test method, until we encounter our next compilation error because the Fee type does not have an initializer that takes a parameter named category.

func testCategory_hasValueEnrollmentPassedInThroughIntitializer() {
    // given
    let testCategory = Fee.Category.enrollment
    // when
    sut = Fee(category: testCategory)
    // ... TO BE CONTINUED
}

We add the following property to our Fee struct to make the test code compile.

var category: Category

Note that we do not need to add an initializer to our struct since Swift automatically provides a member-wise initializer to every struct where an initializer is not explicitly provided in the declaration of the struct.

Having returned to green, we finish writing our first test, as shown below.

func testCategory_hasValueEnrollmentPassedInThroughIntitializer() {
    // given
    let testCategory = Fee.Category.enrollment
    // when
    sut = Fee(category: testCategory)
    // then
    XCTAssertEqual(sut.category, testCategory)
}

This test passes without writing any more production code thanks to the automatically generated member-wise initializer.

Congratulations! We have just written our first passing test. Those new to TDD may be thinking this constant switching between test and production code is a bit tedious. This may be true but it is, in fact, a key feature of the rhythm of TDD. Once we get used to the rapid feedback cycle that TDD enables, it creates a high level of confidence in the correctness of our code. We just have to stick with it, and trust the process, for the benefits to become apparent.

Moving on, we write a test that the Fee initializer can also take a Category value of course with an associated String value and, as in the previous test, this Category value gets stored in the category property.

Before writing this test, we create a new TestConstants class, as shown below.

class TestConstants {
    static let someString = "String"
}

We do this because when initializing or preparing the sut for a particular test, we are sometimes interested in only a subset of the arguments we need to provide. The arguments that we are passing in only to make the code compile can have any valid values. Since we are not interested in the actual values of these arguments, we define a let constant of each relevant type, which we can pass in whenever a value of that type is expected. This keeps the focus on the variables that are relevant for each test while avoiding having to either pepper our tests with literal values that are not relevant to the test or define unnecessary named variables in each test.

In this case, we are only interested in validating that when we initialize a Fee instance with the course case of Category with a certain course title as the associated value, the same Category value gets assigned to the category property of the new instance. We are not interested in what value we use for the course title. So rather than defining a new variable in the test method for the title or using a literal value, neither of which would be relevant to the intention of the test, we use the someString constant we have defined above.

We start writing our test using the let constant someString for the course title.

func testCategory_hasValueCourseWithTitlePassedInThroughInitializer() {
    // given
    let testCategory = Fee.Category.course(TestConstants.someString)
    // ... TO BE CONTINUED
}

We are unable to complete writing our test since the Category enum does not have a course case that takes a String associated value. So we modify the Category enum nested inside the Fee struct, as shown below.

enum Category: Equatable {
    case enrollment
    case course(String)
}

Now we can finish writing our test.

func testCategory_hasValueCourseWithTitlePassedInThroughInitializer() {
    // given
    let testCategory = Fee.Category.course(TestConstants.someString)
    // when
    sut = Fee(category: testCategory)
    // then
    XCTAssertEqual(sut.category, testCategory)
}

This test also passes since the member-wise initializer initializes all the properties with the values provided.

Next, we write a test that the Fee type also has a property named amount which is initialized through a Double value passed in through the initializer. Since in this case we are only interested in the amount we pass in through the initializer and not in the fee category, before writing the test we add the following property to the TestConstants class.

static let someFeeCategory = Fee.Category.enrollment

Now we can start writing our test.

func testAmount_hasValuePassedInThroughInitializer() {
    // given
    let testAmount = 100.0
    // when
    sut = Fee(category: TestConstants.someFeeCategory, amount: testAmount)
    // ... TO BE CONTINUED
}

We get a compilation error because the initializer of the Fee struct has a single parameter whereas we are passing in two arguments. This is easily fixed by adding the following property to the Fee struct.

var amount: Double

Now we have an unintended consequence. The two tests we wrote earlier no longer compile since they expect the Fee initializer to accept a single argument. Now we also need to provide an argument of type Double for the amount parameter. Since we are not interested in the value of this argument for those tests, we define the following property in the TestConstants class.

static let someDouble = 1.0

To make the two previous tests compile again, we change the line in the when section of both the tests where we initialize the sut, as shown below.

// when
sut = Fee(category: testCategory, amount: TestConstants.someDouble)

We run all the tests to make sure everything still passes, and then finish writing the above test as follows.

func testAmount_hasValuePassedInThroughInitializer() {
    // given
    let testAmount = 100.0
    // when
    sut = Fee(category: TestConstants.someFeeCategory, amount: testAmount)
    // then
    XCTAssertEqual(sut.amount, testAmount)
}

This test passes without the need to write any further production code because of the member-wise initializer automatically provided by Swift.

2. Invoices

Next we test for the presence of an Invoice type, instances of which will contain a collection of fees. We begin as before by creating a new test class.

class InvoiceTests: XCTestCase {
    private var sut: Invoice!
    // ... TO BE CONTINUED
}

We have to stop here because the above code does not compile. To remedy this, we create an Invoice type in the production code.

struct Invoice: Equatable {
}

We complete the skeleton of our test class, as shown below.

class InvoiceTests: XCTestCase {
    private var sut: Invoice!
    
    override func tearDown() {
        sut = nil
    }
}

We write our first test for the Invoice type to validate that an invoice has no fees associated with it when it is first created. We do this by asserting that a newly initialized Invoice instance has an empty collection named fees.

func testFees_isEmpty() {
    // when
    sut = Invoice()
    // then
    XCTAssertTrue(sut.fees.isEmpty)
}

This test does not compile since Invoice does not have the required property. Accordingly, we add the following property to the Invoice struct, which makes the above test compile and pass.

var fees = [Fee]()

Note that our test does not require a collection type with any specific features so we have used the Array type, which meets the requirement of the test.

Next, we write a test for a fee to be added to a newly created invoice, which should result in that fee being the only element in the fees collection of the invoice. Note that we use the let constants someFeeCategory and someDouble respectively for the category and amount parameters of the Fee initializer since we are only interested in validating that the Fee instance contained in the fees collection of the invoice is equal to the Fee instance we added to the invoice. We are not interested in the actual values of the arguments we pass in while creating the Fee instance.

func testAddFee_newFeeIsTheOnlyFeeInFees() {
    // given
    let testFee = Fee(category: TestConstants.someFeeCategory, amount: TestConstants.someDouble)
    sut = Invoice()
    // when
    sut.addFee(testFee)
    // ... TO BE CONTINUED
}

Before writing more test code, we must add the following method to the Invoice struct.

func addFee(_: Fee) {
}

Now we can finish writing the test.

func testAddFee_newFeeIsTheOnlyFeeInFees() {
    // given
    let testFee = Fee(category: TestConstants.someFeeCategory, amount: TestConstants.someDouble)
    sut = Invoice()
    // when
    sut.addFee(testFee)
    // then
    XCTAssertEqual(sut.fees.count, 1)
    XCTAssertTrue(sut.fees.contains(testFee))
}

Note that we have written two assertions, one to verify that the fees collection contains only one element, and the other to verify that it contains the fee that was added. We could have achieved the same objective by creating an array with the test fee as its only element, and asserting that the fees collection is equal to this array. This, however, would have created a coupling between the test and implementation details of the production code. This is not a desirable pattern since it can cause test code to become brittle, i.e., tests start to break when we change implementation details of production code without altering its behaviour.

To pass this test, we modify the addFee(_:) method as shown below.

mutating func addFee(_ fee: Fee) {
    fees.append(fee)
}

As stated in the beginning of this article, once we have achieved the green state, i.e., all test we have written pass, we should look for any opportunities to refactor our code. This includes not only production code but also test code. We see that we have some code duplication in the two invoice tests. Both the tests use the same line of code to initialize the sut. To remove this duplication, we restructure the InvoiceTests class to move initialization of the sut to a setUp() method, as shown below.

class InvoiceTests: XCTestCase {
    private var sut: Invoice!
    
    override func setUp() {
        sut = Invoice()
    }
    
    override func tearDown() {
        sut = nil
    }
    
    func testFees_isEmpty() {
        XCTAssertTrue(sut.fees.isEmpty)
    }
    
    func testAddFee_newFeeIsTheOnlyFeeInFees() {
        // given
        let testFee = Fee(category: TestConstants.someFeeCategory, amount: TestConstants.someDouble)
        // when
        sut.addFee(testFee)
        // then
        XCTAssertEqual(sut.fees.count, 1)
        XCTAssertTrue(sut.fees.contains(testFee))
    }
}

Before we write our next test, we add the following properties to TestConstants.

static let someOtherDouble = 2.0
static let someOtherFeeCategory = Fee.Category.course("String")

Now we can write a test that when a second fee is added to an invoice, it gets added to the fees collection of the invoice.

func testAddFee_oneFeeAdded_newFeeAddedToFees() {
    // given
    let testFee = Fee(category: TestConstants.someFeeCategory, amount: TestConstants.someDouble)
    sut.addFee(testFee)
    let secondTestFee = Fee(category: TestConstants.someOtherFeeCategory, amount: TestConstants.someOtherDouble)
    // when
    sut.addFee(secondTestFee)
    // then
    XCTAssertEqual(sut.fees.count, 2)
    XCTAssertTrue(sut.fees.contains(testFee))
    XCTAssertTrue(sut.fees.contains(secondTestFee))
}

This test passes without requiring any change to the production code.

Now we have another opportunity to refactor this test class. The test we have just written uses a locally defined let constant testFee. The test we wrote earlier defines an identical let constant. To remove this duplication, we define the following instance property in the test class.

private let testFee = Fee(category: TestConstants.someFeeCategory, amount: TestConstants.someDouble)

We then rewrite the last test we wrote, as shown below.

func testAddFee_oneFeeAdded_newFeeAddedToFees() {
    // given
    sut.addFee(testFee)
    let secondTestFee = Fee(category: TestConstants.someOtherFeeCategory, amount: TestConstants.someOtherDouble)
    // when
    sut.addFee(secondTestFee)
    // then
    XCTAssertEqual(sut.fees.count, 2)
    XCTAssertTrue(sut.fees.contains(testFee))
    XCTAssertTrue(sut.fees.contains(secondTestFee))
}

We also rewrite the previous test to use the same instance property instead of defining a local let constant.

func testAddFee_newFeeIsTheOnlyFeeInFees() {
    // when
    sut.addFee(testFee)
    // then
    XCTAssertEqual(sut.fees.count, 1)
    XCTAssertTrue(sut.fees.contains(testFee))
}

We run all tests to make sure everything passes.

Next, we write a test that an invoice has a property named totalPayableAmount with the sum of the amounts of all the fees added to the invoice.

func testTotalPayableAmount_hasSumOfAmountsOfFeesAddedToInvoice() {
    // given
    let testAmount = 100.0
    let secondTestAmount = 150.0
    // when
    sut.addFee(Fee(category: TestConstants.someFeeCategory, amount: testAmount))
    sut.addFee(Fee(category: TestConstants.someFeeCategory, amount: secondTestAmount))
    // then
    XCTAssertEqual(sut.totalPayableAmount, testAmount + secondTestAmount)
}

This test fails to compile at the last line since Invoice does not have a totalPayableAmount property. So we add the following property to the Invoice struct.

var totalPayableAmount = 0.0

We run the above test and see it fail. We then make it pass by changing totalPayableAmount to a computed property, as shown below.

var totalPayableAmount: Double {
    fees.reduce(0) { runningTotal, fee in
        runningTotal + fee.amount
    }
}

3. Courses

Now we look at how students would take and complete courses. We begin by creating a test class for a Course type. We start writing the skeleton of our new test class as follows.

class CourseTests: XCTestCase {
    private var sut: Course!
    //... TO BE CONTINUED
}

As we have done with the other types that haven’t been defined in production code yet, we stop writing test code and define the Course type to remove this compilation error.

struct Course: Equatable {
}

Now we can complete the skeleton of the test class.

class CourseTests: XCTestCase {
    private var sut: Course!
    
    override func tearDown() {
        sut = nil
    }
}

We add the first test to this class for the presence of a property named title, which has the value passed in through the initializer of the Course type.

func testTitle_hasValuePassedInThroughInitializer() {
    // given
    let testTitle = "Course title"
    // when
    sut = Course(title: testTitle)
    //... TO BE CONTINUED
}

We can’t finish writing our test since we get a compilation error when we call the non-existent initializer of the Course type. To remove this error, we add the following property to the Course struct.

var title: String

Now we can finish writing the above test.

func testTitle_hasValuePassedInThroughInitializer() {
    // given
    let testTitle = "Course title"
    // when
    sut = Course(title: testTitle)
    // then
    XCTAssertEqual(sut.title, testTitle)
}

This test passes without the need for any addition or modification to the production code.

For our next test, where we test the Course type for the presence of a property named credits, which should have the value passed in through the initializer, we again use the someString constant we had defined earlier in the TestConstants class. This helps us initialize a Course instance for our test without having to worry about providing a new value for the title parameter of the initializer, which is not relevant to this test.

func testCredits_hasValuePassedInThroughInitializer() {
    // given
    let testCredits = 5.0
    // when
    sut = Course(title: TestConstants.someString, credits: testCredits)
    // ... TO BE CONTINUED
}

In a pattern that would have become familiar by now, we have to stop writing the test and add the following property in the Course struct.

var credits: Double

This causes our earlier test to stop compiling since it expects the Course type to have an initializer with a single parameter. We fix this by changing the when section of that test as shown below.

// when
sut = Course(title: testTitle, credits: TestConstants.someDouble)

Now we can finish writing our partly written test, which passes without any further change to the production code.

func testCredits_hasValuePassedInThroughInitializer() {
    // given
    let testCredits = 5.0
    // when
    sut = Course(title: TestConstants.someString, credits: testCredits)
    // then
    XCTAssertEqual(sut.credits, testCredits)
}

4. Enrolled students

With all tests passing, we create a new test class for an EnrolledStudent type. As before, we have to stop after writing the following test code because of a compilation error.

class EnrolledStudentTests: XCTestCase {
    private var sut: EnrolledStudent!
    // ... TO BE CONTINUED
}

We define an EnrolledStudent struct in our production code as shown below to enable us to proceed.

struct EnrolledStudent: Equatable {
}

Now we can finish writing the skeleton for our new test class.

class EnrolledStudentTests: XCTestCase {
    private var sut: EnrolledStudent!
    
    override func tearDown() {
        sut = nil
    }
}

The first test in this class is for a courses property, which should hold an empty collection when an instance is first created.

func testCourses_isEmpty() {
    // when
    sut = EnrolledStudent()
    // then
    XCTAssertTrue(sut.courses.isEmpty)
}

This test does not compile since EnrolledStudent does not have the required property. To remedy this, we add the following property to the EnrolledStudent struct, which makes the above test compile and pass.

var courses = [Course]()

Note that our test does not require a collection type with any specific features so we have used the Array type, which meets the requirement of the test.

Next, we write a test for an enrolled student taking a new course, which should result in the newly taken course being the only course in the courses collection of the EnrolledStudent instance.

func testTakeCourse_newCourseIsTheOnlyCourseInCourses() {
    // given
    let testCourse = Course(title: TestConstants.someString, credits: TestConstants.someDouble)
    sut = EnrolledStudent()
    // when
    sut.takeCourse(testCourse)
    // ... TO BE CONTINUED
}

To finish writing this test, we first add the following method to the EnrolledStudent struct.

func takeCourse(_: Course) {
}

Now we can finish writing the test.

func testTakeCourse_newCourseIsTheOnlyCourseInCourses() {
    // given
    let testCourse = Course(title: TestConstants.someString, credits: TestConstants.someDouble)
    sut = EnrolledStudent()
    // when
    sut.takeCourse(testCourse)
    // then
    XCTAssertEqual(sut.courses.count, 1)
    XCTAssertTrue(sut.courses.contains(testCourse))
}

Note that, as we had done when testing the Invoice type and for the same reasons as explained there, we have written two assertions, one to verify that the courses collection contains one element, and the other to verify that it contains the course that the student has taken.

To pass this test, we modify the takeCourse(_:) method as shown below.

mutating func takeCourse(_ course: Course) {
    courses.append(course)
}

As noted earlier in this article, while writing code we should constantly be on the lookout for opportunities for refactoring not only production code but also test code. Here we can see that in the two tests we have so far added to the EnrolledStudentTests class, we are initializing the sut in exactly the same way. To remove this duplication, we add a setUp() method where we initialize the sut, and remove the initialization code from the tests, as shown below.

class EnrolledStudentTests: XCTestCase {
    private var sut: EnrolledStudent!
    
    override func setUp() {
        sut = EnrolledStudent()
    }
    
    override func tearDown() {
        sut = nil
    }
    
    func testCourses_isEmpty() {
        XCTAssertTrue(sut.courses.isEmpty)
    }
    
    func testTakeCourse_newCourseIsTheOnlyCourseInCourses() {
        // given
        let testCourse = Course(title: TestConstants.someString, credits: TestConstants.someDouble)
        // when
        sut.takeCourse(testCourse)
        // then
        XCTAssertEqual(sut.courses.count, 1)
        XCTAssertTrue(sut.courses.contains(testCourse))
    }
}

As with every refactoring step, we run all tests to make sure nothing has been broken.

Before we write our next test, we add the following property to TestConstants.

static let someOtherString = "Another string"

Now we can write a test that when an enrolled student takes a second course, that course gets added to the courses collection of the EnrolledStudent instance.

func testTakeCourse_oneCourseTaken_newCourseAddedToCourses() {
    // given
    let testCourse = Course(title: TestConstants.someString, credits: TestConstants.someDouble)
    sut.takeCourse(testCourse)
    let secondTestCourse = Course(title: TestConstants.someOtherString, credits: TestConstants.someOtherDouble)
    // when
    sut.takeCourse(secondTestCourse)
    // then
    XCTAssertEqual(sut.courses.count, 2)
    XCTAssertTrue(sut.courses.contains(testCourse))
    XCTAssertTrue(sut.courses.contains(secondTestCourse))
}

This test passes without requiring any change to the production code.

Now we have another opportunity to refactor this test class. The test we have just written uses a locally defined let constant testCourse. The test we wrote earlier also defines and uses a let constant with the same name and value. To remove this duplication, we define the following property in the test class.

private let testCourse = Course(title: TestConstants.someString, credits: TestConstants.someDouble)

We then rewrite the last test we wrote, as shown below.

func testTakeCourse_oneCourseTaken_newCourseAddedToCourses() {
    // given
    sut.takeCourse(testCourse)
    let secondTestCourse = Course(title: TestConstants.someOtherString, credits: TestConstants.someOtherDouble)
    // when
    sut.takeCourse(secondTestCourse)
    // then
    XCTAssertEqual(sut.courses.count, 2)
    XCTAssertTrue(sut.courses.contains(testCourse))
    XCTAssertTrue(sut.courses.contains(secondTestCourse))
}

We also rewrite the previous test to use the same instance property instead of defining a local let constant.

func testTakeCourse_newCourseIsTheOnlyCourseInCourses() {
    // when
    sut.takeCourse(testCourse)
    // then
    XCTAssertEqual(sut.courses.count, 1)
    XCTAssertTrue(sut.courses.contains(testCourse))
}

We run all tests to make sure everything passes.

Next, we test that an enrolled student should not be allowed to attempt to take a duplicate course.

func testTakeCourseWithDuplicateCourse_oneCourseTaken_newCourseNotAddedToCourses() {
    // given
    sut.takeCourse(testCourse)
    // when
    sut.takeCourse(testCourse)
    // then
    XCTAssertEqual(sut.courses.count, 1)
    XCTAssertTrue(sut.courses.contains(testCourse))
}

This test fails because the Array type allows addition of duplicate elements. Rather than creating our own duplication check, we can use the Set type from the standard library which better fits our requirements since a set by definition is an unordered collection of unique elements. Accordingly, we modify the EnrolledStudent struct as shown below.

struct EnrolledStudent: Equatable {
    var courses = Set<Course>()
    
    mutating func takeCourse(_ course: Course) {
        courses.insert(course)
    }
}

This does not compile, however, since the Swift Set type requires that its elements conform to the Hashable protocol. Sets need a way to efficiently test for membership and the Set type does this using hash values. The Hashable protocol ensures that all conforming types provide a way to produce an integer hash value. Swift makes it easy to add Hashable conformance by automatically synthesizing it for user-defined types as long as Hashable conformance is declared when the type is defined and all its stored properties are of types that conform to Hashable. Since both the String and Double types conform to Hashable, all we have to do is declare Hashable conformance in the declaration of the Course type, as shown below.

struct Course: Equatable, Hashable {
    var title: String
    var credits: Double
}

This makes the code compile, and the test we added above passes.

Next, we write a test for an enrolled student to complete a course. This should remove the course from the collection of courses currently being taken by the student. First, we write a test for a student currently taking a single course.

func testCompleteCourse_oneCourseTaken_coursesIsEmpty() {
    // given
    sut.takeCourse(testCourse)
    // when
    sut.completeCourse(testCourse)
    // ... TO BE CONTINUED
}

We cannot finish writing this test until we add the following method to the EnrolledStudent struct.

func completeCourse(_: Course) {
}

Now we can finish writing the test.

func testCompleteCourse_oneCourseTaken_coursesIsEmpty() {
    // given
    sut.takeCourse(testCourse)
    // when
    sut.completeCourse(testCourse)
    // then
    XCTAssertTrue(sut.courses.isEmpty)
}

To make this test pass, we modify the completeCourse(_:) method as shown below.

mutating func completeCourse(_ course: Course) {
    courses.remove(course)
}

We write another test to confirm that if a student is taking two courses, completing one course removes the correct course from the courses collection.

func testCompleteCourse_twoCoursesTaken_coursesHasOnlyCourseNotCompleted() {
    // given
    sut.takeCourse(testCourse)
    let secondTestCourse = Course(title: TestConstants.someOtherString, credits: TestConstants.someOtherDouble)
    sut.takeCourse(secondTestCourse)
    // when
    sut.completeCourse(testCourse)
    // then
    XCTAssertEqual(sut.courses.count, 1)
    XCTAssertTrue(sut.courses.contains(secondTestCourse))
}

This test passes without the need for any change to the production code.

With all greens, we do a bit of refactoring in our test code. Since we have locally defined the let constant secondTestCourse in two tests with exactly the same value, we factor it out and define it as an instance property in the EnrolledStudentTests class.

private let secondTestCourse = Course(title: TestConstants.someOtherString, credits: TestConstants.someOtherDouble)

We can now rewrite the last test we wrote as shown below.

func testCompleteCourse_twoCoursesTaken_coursesHasOnlyCourseNotCompleted() {
    // given
    sut.takeCourse(testCourse)
    sut.takeCourse(secondTestCourse)
    // when
    sut.completeCourse(testCourse)
    // then
    XCTAssertEqual(sut.courses.count, 1)
    XCTAssertTrue(sut.courses.contains(secondTestCourse))
}

We also rewrite the test we wrote earlier that locally defines secondTestCourse to use the same instance property.

func testTakeCourse_oneCourseTaken_newCourseAddedToCourses() {
    // given
    sut.takeCourse(testCourse)
    // when
    sut.takeCourse(secondTestCourse)
    // then
    XCTAssertEqual(sut.courses.count, 2)
    XCTAssertTrue(sut.courses.contains(testCourse))
    XCTAssertTrue(sut.courses.contains(secondTestCourse))
}

We run all the tests to make sure we still have greens all around.

5. Casual students

Now we turn our attention to casual students who are not enrolled but may take courses as and when they like. As before, we start by creating a test class.

class CasualStudentTests: XCTestCase {
    private var sut: CasualStudent!
    // ... TO BE CONTINUED
}

We get an error because the CasualStudent type does not exist, which we define as shown below.

struct CasualStudent: Equatable {
}

This allows us to finish creating the skeleton of the test class.

class CasualStudentTests: XCTestCase {
    private var sut: CasualStudent!
    
    override func tearDown() {
        sut = nil
    }
}

Since there is no difference between how enrolled and casual students take and complete courses, all the tests related to taking and completing courses, and the corresponding production code, will be identical to what we have written above for enrolled students. Whilst in a real project we will still write the tests and the production code line by line, in the interest of space in this article, we will simply copy the test and production code related to taking and completing courses from EnrolledStudentTests to CasualStudentTests, and from EnrolledStudent to CasualStudent.

Here is the CasualStudentTests class after copying the tests related to taking and completing courses along with the setUp() method and the two instance properties that support these tests. The only change as would be expected is that the setUp() method in this case creates an instance of CasualStudent.

class CasualStudentTests: XCTestCase {
    private let testCourse = Course(title: TestConstants.someString, credits: TestConstants.someDouble)
    private let secondTestCourse = Course(title: TestConstants.someOtherString, credits: TestConstants.someOtherDouble)
    
    private var sut: CasualStudent!
    
    override func setUp() {
        sut = CasualStudent()
    }
    
    override func tearDown() {
        sut = nil
    }
    
    func testCourses_isEmpty() {
        XCTAssertTrue(sut.courses.isEmpty)
    }
    
    func testTakeCourse_newCourseIsTheOnlyCourseInCourses() {
        // when
        sut.takeCourse(testCourse)
        // then
        XCTAssertEqual(sut.courses.count, 1)
        XCTAssertTrue(sut.courses.contains(testCourse))
    }
    
    func testTakeCourse_oneCourseTaken_newCourseAddedToCourses() {
        // given
        sut.takeCourse(testCourse)
        // when
        sut.takeCourse(secondTestCourse)
        // then
        XCTAssertEqual(sut.courses.count, 2)
        XCTAssertTrue(sut.courses.contains(testCourse))
        XCTAssertTrue(sut.courses.contains(secondTestCourse))
    }
    
    func testTakeCourseWithDuplicateCourse_oneCourseTaken_newCourseNotAddedToCourses() {
        // given
        sut.takeCourse(testCourse)
        // when
        sut.takeCourse(testCourse)
        // then
        XCTAssertEqual(sut.courses.count, 1)
        XCTAssertTrue(sut.courses.contains(testCourse))
    }
    
    func testCompleteCourse_oneCourseTaken_coursesIsEmpty() {
        // given
        sut.takeCourse(testCourse)
        // when
        sut.completeCourse(testCourse)
        // then
        XCTAssertTrue(sut.courses.isEmpty)
    }
    
    func testCompleteCourse_twoCoursesTaken_coursesHasOnlyCourseNotCompleted() {
        // given
        sut.takeCourse(testCourse)
        sut.takeCourse(secondTestCourse)
        // when
        sut.completeCourse(testCourse)
        // then
        XCTAssertEqual(sut.courses.count, 1)
        XCTAssertTrue(sut.courses.contains(secondTestCourse))
    }
}

Here is the CasualStudent struct, with the courses property and the two methods for taking and completing courses, copied from EnrolledStudent.

struct CasualStudent: Equatable {
    var courses = Set<Course>()
    
    mutating func takeCourse(_ course: Course) {
        courses.insert(course)
    }
    
    mutating func completeCourse(_ course: Course) {
        courses.remove(course)
    }
}

Just to repeat, in a real project we would have written all this test and production code incrementally just as we did for enrolled students. Here we have copied only to save space in this article. Just to make sure everything still works, we run all the tests once again.

6. Refactoring (round 1)

Having written (or in this case, copied) some duplicate code to pass our tests, let’s see how we can refactor this code. This is made easy by Swift since Swift protocols enable us to retroactively introduce abstractions in our code. So we can write concrete code first without having to worry about code duplication or abstractions. This avoids having to lock ourselves into abstractions too early in the development process. This is useful in particular when we are still exploring some of the functionality that we are looking to implement.

As we have noted before, the entire functionality related to taking and completing courses is duplicated in the EnrolledStudent and CasualStudent structs.

We begin by focusing on the functionality related to taking courses, by defining a protocol named CourseTaking, as shown below.

protocol CourseTaking {
    var courses: Set<Course> { get set }
    mutating func takeCourse(_: Course)
}

We then implement the method required by this protocol in a protocol extension. The implementation of the method is identical to what is already contained in the EnrolledStudent and CasualStudent structs, which we remove from these structs.

extension CourseTaking {
    mutating func takeCourse(_ course: Course) {
        courses.insert(course)
    }
}

We make the EnrolledStudent and CasualStudent structs conform to CourseTaking by means of extensions on the structs, as shown below.

extension EnrolledStudent: CourseTaking {}

extension CasualStudent: CourseTaking {}

We run all the tests to make sure the refactoring hasn’t broken anything.

We then do the same for the functionality related to completing courses, starting by adding the following method to the CourseTaking protocol.

mutating func completeCourse(_: Course)

We add the implementation of this new method in the extension on CourseTaking that we have defined above. The implementation of the method is identical to what is already contained in the EnrolledStudent and CasualStudent structs, which we remove from these structs.

mutating func completeCourse(_ course: Course) {
    courses.remove(course)
}

Having completed this round of refactoring, we run all the tests again to make sure everything passes.

7. Generating invoices

The next step is to see how fees will be calculated for enrolled students so they can be invoiced. Because all enrolled students pay the same enrollment fee, to calculate the fees to be paid by enrolled students we first need some place to store the enrollment fee amount. We also need to store the amount of the fee per course credit for enrolled students to calculate course fees for enrolled students who are taking courses.

We could store these fee amounts using instance properties in the EnrolledStudent struct but that would make the design inflexible. Instead we decide to create a Configuration type to store all fee amounts so it will be easier to configure them as required. In the interest of space, we will not get into how these properties could be configured, which can be done in a number of ways, e.g., reading the fee amounts from a configuration file, creating a configuration screen in the user interface, etc. For the purposes of this article, we will just create a Configuration type, with the fee amounts as properties.

The first step is to create a test class for a Configuration type, which will store the fee amounts as static properties. Since there will be no instance properties or methods to test, we don’t need to create an instance of the type to be tested, which in earlier tests we had referred to as the system under test, and stored in a variable called sut. Here we are just testing properties related to the type of the system under test, so we define sutType as a typealias for the Configuration type, as shown below.

class ConfigurationTests: XCTestCase {
    typealias sutType = Configuration
}

This code does not compile because we don’t yet have a Configuration type in the production code. We remedy this by defining it as follows.

class Configuration {
}

Next, we write a test that the enrollment fee amount in the Configuration type is a Double with a value of 200.0. This is just an arbitrary value that we will use for the purposes of this article.

func testEnrollmentFeeAmount_hasDoubleValue200() {
    XCTAssertEqual(sutType.enrollmentFeeAmount, 200.0)
}

We make this test compile, and pass, by adding the following static property to the Configuration class.

static var enrollmentFeeAmount = 200.0

Having created a place for the enrollment fee amount to be used, we turn our attention back to EnrolledStudentTests and add a test for a method that generates an invoice for an enrolled student who is not taking any courses so should pay only the enrollment fee with the same value as the enrollment fee amount in the Configuration type. Here is how far we get before we get a compilation error.

func testGenerateInvoice_notTakingCourses_invoiceHasOnlyEnrollmentFee() {
    // when
    let invoice = sut.generateInvoice()
    // ... TO BE CONTINUED
}

To remove this error, we add the following method to the EnrolledStudent struct.

func generateInvoice() -> Invoice {
    Invoice()
}

Now we can finish writing the test.

func testGenerateInvoice_notTakingCourses_invoiceHasOnlyEnrollmentFee() {
    // when
    let invoice = sut.generateInvoice()
    // then
    let expectedEnrollmentFee = Fee(category: .enrollment, amount: Configuration.enrollmentFeeAmount)
    XCTAssertEqual(invoice.fees.count, 1)
    XCTAssertTrue(invoice.fees.contains(expectedEnrollmentFee))
}

Now we see the test compile and fail, since the generateInvoice() method we wrote just returns an empty invoice. To make the test pass, we modify this method as follows.

func generateInvoice() -> Invoice {
    var invoice = Invoice()
    invoice.addFee(Fee(category: .enrollment, amount: Configuration.enrollmentFeeAmount))
    return invoice
}

Next, we need to write a test for an invoice for an enrolled student who is taking courses. Before we do that, we need to test that the Configuration type has a property for the amount of the fee per course credit to be paid by enrolled students. We add the following test to ConfigurationTests using an arbitrary value for the enrolledStudentCourseCreditFeeAmount property.

func testEnrolledStudentCourseCreditFeeAmount_hasDoubleValue50() {
    XCTAssertEqual(sutType.enrolledStudentCourseCreditFeeAmount, 50.0)
}

The above test fails because this property does not exist. We make the test compile and pass by adding the following static property to the Configuration class.

static var enrolledStudentCourseCreditFeeAmount = 50.0

Now we can write a test to verify that the invoice for an enrolled student who is taking courses contains both an enrollment fee and fees for all the courses being taken by the student, using the enrollment and course credit fee amounts contained in the Configuration class.

func testGenerateInvoice_takingCourses_invoiceHasEnrollmentFeeAndCourseFees() {
    // given
    let testCourseCreditsArray = [1.0, 2.0, 3.0]
    for testCourseCredits in testCourseCreditsArray {
        let course = Course(title: TestConstants.someString, credits: testCourseCredits)
        sut.takeCourse(course)
    }
    // when
    let invoice = sut.generateInvoice()
    // then
    let expectedEnrollmentFee = Fee(category: .enrollment, amount: Configuration.enrollmentFeeAmount)
    XCTAssertEqual(invoice.fees.count, testCourseCreditsArray.count + 1)
    XCTAssertTrue(invoice.fees.contains(expectedEnrollmentFee))
    for testCourseCredits in testCourseCreditsArray {
        let expectedCourseFee = Fee(category: .course(TestConstants.someString), amount: testCourseCredits * Configuration.enrolledStudentCourseCreditFeeAmount)
        XCTAssertTrue(invoice.fees.contains(expectedCourseFee))
    }
}

The test shows multiple failures since the generateInvoice() method as written above only considers the enrollment fee. To make the test pass, we modify the generateInvoice() method in EnrolledStudent as shown below.

func generateInvoice() -> Invoice {
    let enrollmentFee = Fee(category: .enrollment, amount: Configuration.enrollmentFeeAmount)
    let courseFees = courses.map() { course in
        Fee(category: .course(course.title), amount: course.credits * Configuration.enrolledStudentCourseCreditFeeAmount)
    }
    return ([enrollmentFee] + courseFees).reduce(into: Invoice()) { invoice, fee in
        invoice.addFee(fee)
    }
}

The main difference between enrolled and casual students is in the calculation of fees. Casual students don’t pay an enrollment fee so the invoice for a casual student should have no fees when the student is not taking any courses. We add a test for this in CasualStudentTests as shown below.

func testGenerateInvoice_notTakingCourses_invoiceDoesNotHaveAnyFee() {
    // when
    let invoice = sut.generateInvoice()
    // ... TO BE CONTINUED
}

We get an error because CasualStudent does not have a generateInvoice() method, which we add as shown below.

func generateInvoice() -> Invoice {
    Invoice()
}

This allows us to complete, and pass, the test shown below.

func testGenerateInvoice_notTakingCourses_invoiceDoesNotHaveAnyFee() {
    // when
    let invoice = sut.generateInvoice()
    // then
    XCTAssertTrue(invoice.fees.isEmpty)
}

Our last test is for a casual student who is taking courses. In such a case, the invoice should contain fees for all the courses the student is taking. Since casual students pay a different course credit fee from enrolled students, however, we first need to add a property to the Configuration class to represent the amount of the course credit fee to be paid by casual students.

We add the following test to ConfigurationTests, using an arbitrary value for the amount of the course credit fee for casual students.

func testCasualStudentCourseCreditFeeAmount_hasDoubleValue75() {
    XCTAssertEqual(sutType.casualStudentCourseCreditFeeAmount, 75.0)
}

To make this test compile and pass, we add the following property to the Configuration class.

static var casualStudentCourseCreditFeeAmount = 75.0

Now we can add the test shown below to CasualStudentTests.

func testGenerateInvoice_takingCourses_invoiceHasCourseFees() {
    // given
    let testCourseCreditsArray = [1.0, 2.0, 3.0]
    for testCourseCredits in testCourseCreditsArray {
        let course = Course(title: TestConstants.someString, credits: testCourseCredits)
        sut.takeCourse(course)
    }
    // when
    let invoice = sut.generateInvoice()
    // then
    XCTAssertEqual(invoice.fees.count, testCourseCreditsArray.count)
    for testCourseCredits in testCourseCreditsArray {
        let expectedCourseFee = Fee(category: .course(TestConstants.someString), amount: testCourseCredits * Configuration.casualStudentCourseCreditFeeAmount)
        XCTAssertTrue(invoice.fees.contains(expectedCourseFee))
    }
}

As expected, this tests shows multiple failures so we modify the generateInvoice() method in CasualStudent as follows.

func generateInvoice() -> Invoice {
    courses.map() { course in
        Fee(category: .course(course.title), amount: course.credits * Configuration.casualStudentCourseCreditFeeAmount)
    }.reduce(into: Invoice()) { invoice, fee in
        invoice.addFee(fee)
    }
}

8. Refactoring (round 2)

In this round, we will refactor the functionality related to generating invoices. Note that we don’t currently have any code duplication in the EnrolledStudent and CasualStudent structs so this refactoring is not required from that perspective. What we will do is extract the code related to generating invoices from the structs and put it into protocol extensions. This not only further demonstrates the protocol-oriented approach to programming but also shows a design that could prove more flexible and scalable over time.

We start by creating a protocol named Invoicable, which contains the generateInvoice() method.

protocol Invoicable {
    func generateInvoice() -> Invoice
}

We know that fee calculation for enrolled students is different from that for casual students. Therefore, we need a way for the Invoicable protocol to use different implementations of generateInvoice() depending on whether the student for whom the fees are being calculated is enrolled or casual.

We deal first with enrolled students by defining the following protocol.

protocol Enrolled {}

This protocol does not have any functionality in itself but it will allow us to define a constrained extension on the Invoicable protocol.

Next we make EnrolledStudent conform to the above protocol through the following extension.

extension EnrolledStudent: Enrolled {}

We define a constrained extension on the Invoicable protocol to cater to enrolled students who are taking courses, moving the generateInvoice() method from the EnrolledStudent struct to the protocol extension. Note that the constraint we have added to the protocol extension means that it applies only when the conforming type also conforms to both the Enrolled and CourseTaking protocols. This makes the courses property of CourseTaking accessible to the generateInvoice() method.

extension Invoicable where Self: Enrolled & CourseTaking {
    func generateInvoice() -> Invoice {
        let enrollmentFee = Fee(category: .enrollment, amount: Configuration.enrollmentFeeAmount)
        let courseFees = courses.map() { course in
            Fee(category: .course(course.title), amount: course.credits * Configuration.enrolledStudentCourseCreditFeeAmount)
        }
        return ([enrollmentFee] + courseFees).reduce(into: Invoice()) { invoice, fee in
            invoice.addFee(fee)
        }
    }
}

Finally, we make EnrolledStudent conform to the Invoicable protocol, as shown below.

extension EnrolledStudent: Invoicable {}

After running all tests to make sure nothing has been broken, we turn our attention to casual students.

Following the pattern we have used for enrolled students, we start by defining a Casual protocol, and make CasualStudent conform to it.

protocol Casual {}

extension CasualStudent: Casual {}

Then we define a constrained extension on the Invoicable protocol for casual students who are taking courses, this time moving the generateInvoice() implementation from the CasualStudent struct.

extension Invoicable where Self: Casual & CourseTaking {
    func generateInvoice() -> Invoice {
        courses.map() { course in
            Fee(category: .course(course.title), amount: course.credits * Configuration.casualStudentCourseCreditFeeAmount)
        }.reduce(into: Invoice()) { invoice, fee in
            invoice.addFee(fee)
        }
    }
}

Finally, we make CasualStudent conform to the Invoicable protocol, as shown below.

extension CasualStudent: Invoicable {}

As has been our practice, we run all the tests again to make sure everything passes.

This completes our refactoring, which has taken all the functionality out of the EnrolledStudent and CasualStudents structs and placed it in protocol extensions. The refactored EnrolledStudent and CasualStudent structs just provide storage for the courses property.

struct EnrolledStudent: Equatable {
    var courses = Set<Course>()
}

struct CasualStudent: Equatable {
    var courses = Set<Course>()
}

This demonstrates not only the protocol-oriented approach to programming that Swift enables but also the ease and confidence with which we can refactor our code in fundamental ways when we have tests that validate every aspect of functionality provided by the code. This, as noted earlier in the article, is one of the key benefits of using TDD.

Conclusion

TDD helps us write code that is not only less prone to bugs but also easier to extend and maintain. Those new to TDD may be surprised at first by the seemingly slow pace at which new code gets written and the constant switching between writing test and production code. This, however, is the foundation of the discipline and rhythm of TDD which becomes second nature over time, and programmers used to this approach can find it hard to write code for a new feature without first writing a test for it.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on X.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Comments

  1. Adnan says

    September 6, 2023 at 8:21 am

    Thank you for such an incredibly well explained tutorial, you’ve really made the benefits of TDD shine.

    The style of teaching was ideal for me, it felt like ‘pair programming’ where a test was written and I was coding the implementation.

    Please do write more such tutorials.

    Reply
    • Khawer Khaliq says

      September 10, 2023 at 10:12 am

      Hi Adnan,

      Thanks for you kind words. Glad that you found the article useful.

      It is comments like yours that provide the motivation to keep writing. Do keep reading and providing feedback.

      Cheers,
      Khawer

      Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Pattern Matching With Optionals in Swift

The optional pattern explained in detail, including how to use it with a variety of conditional statements and loops, and how to add extra conditions when required, with a section on creating more complex pattern matching code involving optionals.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

Unit Testing and UI Testing in Swift

Learn how to use unit testing to gain confidence in the correctness of code at the unit level, and use UI testing to ensure that the application fulfills user requirements, explained in detail and illustrated using an example application built using SwiftUI.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

What Are Swift Optionals and How They Are Used

This article explains what Swift optionals are, why Swift has them, how they are implemented, and how Swift optionals can be used to better model real-world domains and write safer and more expressive code.

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Use protocol-oriented programming to avoid having to use associated types in many situations but also to effectively use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Better Generic Types in Swift With the Numeric Protocol

Covers use of the Numeric protocol as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, using protocol composition to add relevant functionality as required.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part Two)

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.