Efficient Test Data Creation in Swift

Giuseppe Basile
4 min readApr 15, 2018

--

On applications that solve complex domain problems is usual that your domain objects can get bigger and structured. This article describes an approach to simplify the creation of these objects on unit tests using one of the feature introduced in Swift: the default values on functions’ parameters.

Our goals are simplify as much as possible the creation of such objects and increase the readability of our tests.

Let’s say we have this model in our project:

struct Article {
let author: Author
let title: String
let body: String
let section: Section
let published: Date
let shareURL: URL?
}
struct Author {
let name: String
let bio: String
let mainSection: Section
}
struct Section {
let name: String
}

This is not really complicated, but let’s see what happen if we want to use one of the article from our tests.

Imagine we need to test that the ViewModel is creating the correct title.

func testTitleIsCreatedCorrectly() {
let section = Section(name: "Section Name")

let author = Author(name: "Giuseppe",
bio: "Swift Developer",
section: section)
let article = Article(author: author,
title: "Article Title",
body: "Article Body",
section: section,
published: Date(),
shareURL: nil)
let viewModel = ArticleViewModel(article: article) XCTAssertEqual(viewModel.title, "Article Title")
}

There are many problems with this test:

  1. The creation of the subject under test is complex
  2. We are exposing the reader to objects and parameters that are not going to change the result of the tests.
  3. Any change in these 3 objects will require all our tests to be updated, new parameters might be introduced/deleted, new objects might be required to create an article

One common way to solve this problem is to refactor this test and extract the creation of the object on a private method on the test class. In this article, we are going to propose an evolution of this approach that is more focused on reusability and readability.

Simplifying Test Object creation

In our test target, we are going to extend all our domains objects to facilitate their creations using the power of functions with default parameters.

The goal is to be able to create our test objects in one line and to give the reader of our code just the right context to understand what is required for our tests.

extension Section {
static func testData(name: String = "Section Name") -> Section {
return Section(name: name)
}
}

As promised, we can now create a section with just a line of code:

let section = Section.testData()

This is our first reusable factory method, it might not look really helpful but we can now reuse it on others factories as well. Thing become interesting when we can also compose our new methods using the existing factories: let’s see what happen when we apply the same pattern to the Author

extension Author {
static func testData(name: String = "Giuseppe",
bio: String = "Swift Developer",
mainSection: Section = Section.testData()) -> Author {
return Author(name: name,
bio: bio,
mainSection: section)
}
}

Now is possible to create an Author with just a line of code:

let author = Author.testData()

We don’t have to worry anymore of name, bio, and section unless this information is relevant in our tests, if I need to have an author that belongs to a specific section I can now do it with a couple of lines of code:

let section = Section.testData(name: "Test Section")
let author = Author.testData(section: section)

In a Nutshell

  • testData() is a static function that create an object
  • Only one testData() for type
  • testData() can create an object passing no arguments as all its property have default values
  • default values reuse existing testData() whatever is possible
  • default values use nil when the property is optional

We can now use these rules to create testData() for the Article

extension Article {
static func testData(author: Author = Author.testData(),
title: String = "Article Title",
body: String = "Article Body",
section: Section = Section.testData(),
published: Date = Date(),
shareURL: URL? = nil) -> Article {
return Article(author: author,
title: title,
body: body,
section: section,
published: published,
shareURL: shareURL
)
}

All the domain types are implementing testData and we can finally refactor the existing test:

func testPublishedDateIs5MinutesAgo() {
let article = Article.testData(title: "Article Title")
let viewModel = ArticleViewModel(article: article)
XCTAssertEqual(viewModel.title, "Article Title")
}

Conclusions

This test is now concise, readable and contains just the relevant instructions. TestData() facilitate maintenance: as it abstracts away the creation of your test objects in one single place, you only have to update this method when your model change. Test data is reusable and composable.

Default parameters in Swift are a powerful tool useful to simplify the creation of the objects we use in our tests. Optional and required parameters are really important in our application logic but not in our tests. We should take shortcuts when we can avoid writing code that is not relevant while testing. Short and readable tests are the best documentation you are going to have in your project.

--

--