🐼
AutoMockableの自動生成するコードは長いが型にすればいいのかもしれない
はじめに
iOSアプリ開発でテストコードを作成する時、AutoMockableというライブラリを使うこともあるんですが、
このライブラリが自動生成するコードがメソッド名と引数名によって長くなってしまう。なのでその改善案/代替案を型で表現することを考えてるっていう話です。
具体例
具体的にはMyProtocolがあるとき
protocol MyProtocol {
func sayHelloWith(name: String)
}
これが自動で次のようなコードを生成する。しかし、sayHelloWithNameReceivedInvocations
やsayHelloWithNameReceivedName
変数はこれだけで結構長い。
class MyProtocolMock: MyProtocol {
//MARK: - sayHelloWith
var sayHelloWithNameCallsCount = 0
var sayHelloWithNameCalled: Bool {
return sayHelloWithNameCallsCount > 0
}
var sayHelloWithNameReceivedName: String?
var sayHelloWithNameReceivedInvocations: [String] = []
var sayHelloWithNameClosure: ((String) -> Void)?
func sayHelloWith(name: String) {
sayHelloWithNameCallsCount += 1
sayHelloWithNameReceivedName = name
sayHelloWithNameReceivedInvocations.append(name)
sayHelloWithNameClosure?(name)
}
}
長いと何が困るかというとテストコードで呼び出す側のコードが読みづらくなるんすわ。
解決案: 型にする
このMyProtocolMockのプロパティをもっと型を利用した感じにしたのが次の通り。
class MyProtocolMock: MyProtocol {
struct SayHelloWith: Equatable {
struct Input: Equatable {
var name: String
}
var input: Input? {
invocations.last
}
var invocations: [Input] = []
var callsCount: Int {
invocations.count
}
var called: Bool {
!invocations.isEmpty
}
}
var sayHelloWith: SayHelloWith?
func sayHelloWith(name: String) {
if sayHelloWith != nil {
sayHelloWith?.invocations.append(SayHelloWith.Input(name: name))
} else {
sayHelloWith = SayHelloWith(invocations: [.init(name: name)])
}
}
}
型にする際のポイント
- 入力の型を作る
- そもそもcountとか動的にinvocations数えれば良いのでは?
- calledは0より大きいかどうかより
!isEmpty
使えばいいのでは? - invocationsに入ってるんだからcountと同じでnameも個別にセットする必要がないのでは?
- メソッドを呼ばれた際の型SayHelloWithをEqutableにすると良いかも
「メソッドを呼ばれた際の型SayHelloWithをEqutableにすると良いかも」というのはAssertionする際に便利になるはずで、具体的にはこういうことができると思います。
assertEqual(mock.usayHelloWith, SayHelloWith(invocations: [.init(name: "A")]))
// apple/swift-testingでやるなら
#expect(
mock.usayHelloWith == SayHelloWith(invocations: [.init(name: "A")])
)
おわりに
AutoMockableをforkする前に意見が聞きたいので書いてみました。また、他にもテストコードが別スレッドで実行された際に自動生成されたコードがスレッドセーフでないためにデータ競合する課題も感じていて(テスト実行するとEXC_BADACESSとなる)、やるんだったらまだ別の改善も必要かなと思っています。
ほかには、AutoMockableではなくマクロでやるという手段もあるかもしれません。swift-sypableというライブラリがあります。
なんか意見があったらください。2023/10/27のios test nightで直接会った時に直接なんか意見もらうのもいいかもしれないです。
Discussion
関数名でローカルスコープに型を定義してしまうと、同名の型をシャドウしてしまう問題があります。
例えば
のようなプロトコルに対したコード生成結果を考えた場合に、todayが参照するDateが生成された関係のないDateに変化してしまいます。
幸い、被らないような複雑な型名を利用したり、型自体は別のスコープに定義するなどすれば回避できます。