post Image
【iOS】アプリ側でletなstructのpropertyにテスト側で値をセットする

はじめに

propertyがletで定義されているstructのテストデータを用意するときに、都度すべての値を渡してオブジェクトを生成するのが煩わしく感じています。

struct User: AutoTestableSetter {
    let name: String
    let age: Int
    // more properties...
}

そのため下記のように必要な値のみを代入して、簡単にテストデータを生成できるようにしたいと思います。
(ここではpropertyをvarで定義するという手段は取らないということが前提になっています)

var user = User.mock()
print(user.age) // 1
try? user.set(5, for: \.age)
print(user.age) // 5

propertyがletで定義されていても代入できるようにする

propertyがletで定義されていても代入できるようにするには、 https://github.com/Zewo/Reflection を利用することで簡単にできます。

ReflectionはSwift Package ManagerまたはCocoapodsからインストールすることで利用できるようになります。

import Reflection

var user = User.mock()
try? set(5, key: "age", for &user)

ただ、Reflectionをそのまま利用すると

  • valueの型とpropertyが1対1になっていない
  • property名を文字列で指定している

という状態なため、テスト実装時に入力ミスをしやすくなります。

Swift4のKeyPathを利用できるようにする

下記のように、Swift4のKeyPathを利用したsetterを実装することで、値を再代入する側では入力ミスをなくすことができます。

extension User {
    mutating func set<Value>(_ value: Value, for keyPath: KeyPath<User, Value>) throws {
        switch keyPath {
        case \User.name:
            try Reflection.set(value, key: "name", for: &self)
        case \User.age:
            try Reflection.set(value, key: "age", for: &self)
        // more cases...
        default:
            throw ReflectionError.instanceHasNoKey(type: Value.self, key: "\(keyPath)")
        }
    }
}

しかし、上記のようなsetterを実装したとしても、keyPathkeyの対応を間違ってしまう可能性があります。
そこで、 https://github.com/krzysztofzablocki/Sourcery を利用して上記の実装を自動生成しようと思います。

今回は、CocoaPods経由でSourceryをインストールしている前提で進めていきます。

まずは、structで採用するためのprotocolのTestableSetterと自動生成のための識別子となるprotocolのAutoTestableSetterを定義します。

TestableSetter.swift
protocol TestableSetter {
    mutating func set<Value>(_ value: Value, for keyPath: KeyPath<Self, Value>) throws
}

protocol AutoTestableSetter {}

次に、AutoTestableSetterをUserに採用し、Sourcery側でTestableSetterを適用できる状態にします。

User.swift
extension User: AutoTestableSetter {}

そして、Sourceryのテンプレートを生成します。
テンプレートは任意のディレクトリに配置し、コードを生成コマンドを実行する際に指定します。
テンプレート内では、AutoTestableSetter採用している型に対して、TestableSetterを採用し、set関数を実装しています。
KeyPathをしてする際のproperty名(\User.nameなど)とkeyで指定する文字列が一致しているため、自動生成することによって、入力のミスをなくすことができます。

AutoTestableSetter.stencil
import Reflection
@testable import 'Your Host Application Name'

{% for type in types.implementing.AutoTestableSetter %}
// MARK: {{ type.name }} TestableSetter
extension {{type.name}}: TestableSetter {
    mutating func set<Value>(_ value: Value, for keyPath: KeyPath<{{type.name}}, Value>) throws {
        switch keyPath {
        {% for variable in type.storedVariables %}
        case \{{type.name}}.{{variable.name}}:
            try Reflection.set(value, key: "{{variable.name}}", for: &self)
        {% endfor %}
        default:
            throw ReflectionError.instanceHasNoKey(type: Value.self, key: "\(keyPath)")
        }
    }
}
{% endfor %}

Sourceryの生成コマンドを実行します。

  • –sourcesで元のソースコードが配置されているディレクトリ(またはファイル)を指定
  • –templetesでテンプレートが配置されているディレクトリ(またはファイル)を指定
  • –outputで出力するを配置するディレクトリ(またはファイル)を指定
./Resources/sourcery/bin/sourcery --sources ./ProjectName --templates ./ProjectName/Templates/ --output ./ProjectName/Autogenerated

–outputで指定したディレクトリに、自動生成されたファイルが配置されます。

AutoGenerated.Swift
// Generated using Sourcery 0.13.1 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

import Reflection
@testable import 'Your Host Application Name'

// MARK: User TestableSetter
extension User: TestableSetter {
    mutating func set<Value>(_ value: Value, for keyPath: KeyPath<User, Value>) throws {
        switch keyPath {
        case \User.name:
            try Reflection.set(value, key: "name", for: &self)
        case \User.age:
            try Reflection.set(value, key: "age", for: &self)
        // more cases...
        default:
            throw ReflectionError.instanceHasNoKey(type: Value.self, key: "\(keyPath)")
        }
    }
}

一度この設定をしてbuild時のRunScriptで実行するようにしまえば、structが増えたとしてもAutoTestableSetterを採用するだけになり、都度set関数を手入力で実装する必要がなくなります。


『 Swift 』Article List