Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 19

Unit Testing Business Logic with AAA

Recommended resources

Hello! Welcome to the first lesson in our module on "Effective Unit Testing."

In the last module, we explored advanced architectural patterns like MVVM and VIPER. A primary reason for adopting these patterns is to improve the testability of our code by separating business logic from the UI. Today, we begin to capitalize on that separation.

Our goal is to write isolated unit tests for business logic using the Arrange-Act-Assert pattern. This is a foundational skill for any professional developer. Writing good tests ensures your code works as expected, gives you the confidence to refactor and add new features without introducing regressions, and serves as living documentation for your app's logic.

Why Unit Testing Matters

As an experienced developer, you understand that building features is only part of the job. Ensuring those features are robust, maintainable, and bug-free is what elevates your work to a professional standard. Unit tests are one of the most effective tools for achieving this. They allow you to verify the smallest pieces of your application's logic in isolation, catching issues early in the development cycle.

First, let's clarify what we mean by "unit tests" and how they differ from other forms of testing, like UI tests.

Unit Testing a SwiftUI application in Xcode | Advanced Learning #17

The video 'Unit Testing a SwiftUI application in Xcode' by Swiftful Thinking provides a clear distinction between unit tests and UI tests and explains why unit tests are so crucial for a healthy codebase.

Watch the segment from 01:27 to 07:26. Focus on the explanation of what unit tests cover (business logic, view models, data services) versus what UI tests cover (user interaction flows).

As the video explained, unit tests focus on your code's internal logic, while UI tests simulate user interactions. In this module, we will focus exclusively on unit tests, as they are fundamental to verifying the correctness of the business logic you've worked to isolate with architectural patterns.


Setting Up Your Test Environment

To write unit tests, you need a separate "test target" in your Xcode project. This target contains your test code and has access to your main application's code, but it is not compiled into the final app that you ship to the App Store.

If your project doesn't already have one, you can easily add it.

Unit Testing a SwiftUI application in Xcode | Advanced Learning #17

Let's return to the Swiftful Thinking video to see how to add a unit test target to a project and what the initial setup looks like.

Watch from 11:37 to 14:09. This part demonstrates creating a new test target and explains why test files are kept separate from your main app files.

A key piece of the setup is the @testable import YourAppName statement at the top of your test files. Since your app's code is in a separate module, this keyword makes the internal types and methods from your main app target accessible to your test target. Without it, you could only test public declarations.


The Anatomy of a Unit Test: Arrange-Act-Assert

A well-structured unit test is predictable and easy to read. The most common and effective structure is the Arrange-Act-Assert pattern (also known as Given-When-Then).

The Anatomy of a Unit Test (Arrange-Act-Assert Pattern)
This diagram shows the three distinct phases of a well-structured unit test: setting up the context (Arrange), performing the action (Act), and verifying the outcome (Assert).

Here’s what each step entails:

  • Arrange (or Given): Set up the world for your test. This involves initializing the object you want to test (the "System Under Test" or SUT) and creating any data or dependencies it needs.
  • Act (or When): Execute the specific piece of code you are testing. This is typically a single method call on your SUT.
  • Assert (or Then): Verify that the outcome of the Act step is what you expected. This is done using a family of XCTAssert functions provided by Apple's XCTest framework.

This structure ensures each test is focused, readable, and clearly states its purpose. Let's see how this structure, along with a descriptive naming convention, is applied in practice.

Unit Testing a SwiftUI application in Xcode | Advanced Learning #17

This next segment from the Swiftful Thinking video introduces a standard naming convention for tests and demonstrates setting up the Arrange-Act-Assert (or Given-When-Then) structure in code.

Watch from 17:01 to 20:31. Pay close attention to the naming convention test_UnitOfWork_StateUnderTest_ExpectedBehavior and how comments are used to clearly delineate the 'Given', 'When', and 'Then' sections.

To summarize the key points from the video and other best practices:

  • Test Naming: Test methods must begin with the prefix test. A descriptive name like test_ViewModel_isPremium_shouldBeTrue immediately tells another developer what this test is verifying.
  • Test Structure: Use comments (// Given, // When, // Then or // Arrange, // Act, // Assert) to organize the logic within your test method. This makes your tests incredibly easy to follow.

Writing Your First Tests

Now, let's put theory into practice. We'll start by testing a simple ViewModel with a boolean property. This will help you get comfortable with the workflow and basic assertion functions.

Unit Testing a SwiftUI application in Xcode | Advanced Learning #17

Let's watch how to write and run our first simple unit tests. This segment will cover testing a boolean property, using basic assertions, and the importance of testing different inputs.

Watch from 20:31 to 28:50. This practical demonstration will show you how to use XCTAssertTrue, XCTAssertFalse, and XCTAssertEqual to validate the state of your ViewModel after it's initialized.

As you saw, the process is straightforward:

  1. Arrange: Create an instance of the UnitTestingBootcampViewModel, passing in a specific value (e.g., true).
  2. Act: The Act step is implicit here, as the logic we're testing is in the init method itself.
  3. Assert: Use XCTAssertTrue(vm.isPremium) to check if the property was set correctly.

The video also makes an important point about using specialized assertions. Instead of XCTAssert(x == y), it's better to use XCTAssertEqual(x, y). This provides more informative failure messages in the test logs, telling you exactly what the differing values were.


Testing Business Logic and Edge Cases

Simple property assignment is one thing, but the real value of unit testing comes from verifying more complex business logic and, crucially, edge cases. Professional-grade code anticipates and correctly handles scenarios like invalid input, empty data, and unexpected sequences of events.

Let's expand our ViewModel with functions to manage an array of strings and see how to test them.

Unit Testing a SwiftUI application in Xcode | Advanced Learning #17

This next segment demonstrates how to test functions that modify state, such as adding items to an array. It also shows how to write a specific test for an edge case—preventing blank strings from being added.

Watch from 31:31 to 43:02. Notice how different assertions (XCTAssertEqual, XCTAssertFalse, XCTAssertGreaterThan) are used to verify the state of the dataArray. Pay special attention to the test dataArray_shouldNotAddBlankString, as this is a perfect example of testing an edge case.

This part highlights several important practices:

  • Test one thing at a time: Each test method should verify a single concern. There's one test for adding items and a separate, dedicated test for the blank string edge case.
  • Multiple Assertions: It's acceptable to have multiple assertions in a single test if they all verify the same logical outcome. For example, asserting that an array's count is 1 and that its isEmpty property is false are both checks related to the single outcome of successfully adding an item.
  • Testing for nil: The video also shows tests for selectedItem, demonstrating XCTAssertNil and XCTAssertNotNil, which are essential for testing logic involving optional values.
  • Testing throwing functions: You can use XCTAssertThrowsError to verify that a function correctly throws an error under specific conditions, and even assert that the thrown error is of a specific type.

Ensuring Test Isolation with setUp and tearDown

A fundamental rule of unit testing is that tests must be isolated. The outcome of one test should never be affected by a test that ran before it. XCTest provides two special methods to help with this: setUpWithError() and tearDownWithError().

Unit Testing in iOS with XCTest

The video 'Unit Testing in iOS with XCTest' by Etisha Garg gives a concise explanation of how setUp and tearDown work to create a clean environment for every single test.

Watch this segment from 05:54 to 09:34. Focus on the explanation of why each test needs to run in a 'virtual clean room' and how setUp is used to initialize the System Under Test (SUT) and tearDown is used to clean it up.

Here's the lifecycle for each test method you run:

  1. setUpWithError() is called. You create a fresh instance of your SUT here.
  2. Your test...() method is called.
  3. tearDownWithError() is called. You deallocate your SUT here by setting it to nil.

This cycle guarantees that every test starts with a known, clean state, which is critical for reliable and deterministic tests. While initializing the SUT inside each test method (as seen in the Swiftful Thinking video) also works, using the setUp/tearDown pattern becomes more efficient when you have complex setup logic that is shared across many tests.


Conclusion

Congratulations on completing your first lesson in unit testing! You've learned the fundamental principles and patterns that are the bedrock of writing effective tests.

Key Takeaways:

  • Unit tests are automated tests that verify small, isolated pieces of your app's business logic.
  • The Arrange-Act-Assert pattern provides a clear, readable structure for all your tests.
  • A descriptive naming convention (test_UnitOfWork_State_ExpectedBehavior) makes your tests self-documenting.
  • Use the most specialized assertions (XCTAssertEqual, XCTAssertNil, etc.) to get informative failure messages.
  • Always test for edge cases (e.g., empty strings, nil values, invalid inputs) to build a robust application.
  • Use setUpWithError() and tearDownWithError() to ensure each test runs in isolation from the others.

Preview of the Next Lesson:
In this lesson, we tested a ViewModel that had no external dependencies. However, most real-world objects do. For example, a ViewModel might depend on a network service to fetch data. To test the ViewModel in isolation, we need a way to replace that real network service with a "test double." In the next lesson, you will learn to differentiate between stubs, fakes, and mocks, and create a mock object to isolate a dependency.