先月、pointfreeから新たなライブラリである, swift-macro-testingが出ました。このライブラリを使ってみたところ、かなり便利だったので、今回は従来のマクロのテストコードで起こっていた不便な点と、swift-macro-testingを使用することでどういった点が便利になるのかなどを紹介していきたいと思います。
また、今回は@PublicInit
マクロを例にしていきたいと思います。
@PublicInit
は私のライブラリであるswift-dependencies-macroの中に入っているので、よかったらコードを見てみてください。
従来のマクロのテスト
まずは従来のマクロのテストフレームワークであるSwiftSyntaxMacrosTestSupport
を使用して,マクロのテストを書いてみます。
let macros: [String: Macro.Type] = [
"PublicInit": PublicInitMacro.self
]
func testPublicInit() {
assertMacroExpansion(
"""
@PublicInit
public struct Test {
let a: String
let b: () -> Void
let c: @Sendable () -> Void
}
""",
expandedSource:
"""
public struct Test {
let a: String
let b: () -> Void
let c: @Sendable () -> Void
public init(
a: String,
b: @escaping () -> Void,
c: @Sendable @escaping () -> Void
) {
self.a = a
self.b = b
self.c = c
}
}
""",
macros: macros
)
}
上記のコードでビルドするとしっかりとテストが通ります.
Diagnosticのテスト
では、Diagnosticのテストはどうでしょうか。コードを書いてみます。
assertMacroExpansion(
"""
@PublicInit
public class Test {
}
""",
expandedSource:
"""
public class Test {
}
""",
diagnostics: [
DiagnosticSpec(
message: "PublicInit Macro can only be applied to struct.",
line: 2,
column: 8
)
],
macros: macros
)
DiagnosticのテストをするにはDiagnosticSpecを使用します。DiagnosticSpecのinitializerには、エラーメッセージ, エラーが出るであろう該当箇所の行数とカラムを指定できます。
今回のコードでは、このように2行目の左から8個目の箇所でエラーが発生していることがわかります。
なので、lineには2, columnには8を指定します。
そして、このコードを実行するとテストが通ります。
FixItのテスト
次にFixItのテストを書いてみましょう。
FixItとは, エラーメッセージに付属してあるFixオプションのことで、Syntaxの修正をすることができるものです。
こちらをテストするには以下のようなコードを書きます。
assertMacroExpansion(
"""
@PublicInit
public struct Test {
let test = ""
}
""",
expandedSource:
"""
public struct Test {
let test = ""
public init(
) {
}
}
""",
diagnostics: [
DiagnosticSpec(
message: "PublicInit Macro required stored properties provide explicit typed annotations.",
line: 3,
column: 9,
fixIts: [
FixItSpec(
message: "Insert type annotation."
)
]
)
],
macros: macros
)
Diagnosticのテストと同じく, DiagnosticSpecを使用してテストを書きます。
しかし、FixItSpecはFixItを挿入後の構文のテストをすることができず、現在はメッセージのテストのみのサポートのようです。ただし、公式のドキュメントの方には将来的にサポートする予定と書いてあったので、今後のサポートに期待しましょう。
不便な点
従来のテストAPIを使用していくと、いくつか不便な点に直面します。
1つ目は、先ほど述べたとおりFixItを挿入後の構文のテストをすることができないと言う点です。
2つ目は、絶望的にDiagnosticのテストがわかりずらいと言う点です。lineとcolumnをみて該当の行数まで数えて, さらに左から○番目まで数えて, エラーを出したい箇所まで辿り着かないといけません。非常にめんどくさいですし、視覚的にわかりずらいです。
そして3つ目は、マクロに変更があった際にテストコードの変更を毎回しないといけないと言う点です。
仮に何個もテストコードを書いていた際のことを考えます。
@PublicInit
マクロでは、struct以外の宣言にはマクロを適用することができません。
なので、以下のテストコードがclass以外にもactor, enum, extension, protocolと5つ必要です。
assertMacroExpansion(
"""
@PublicInit
public class Test {
}
""",
expandedSource:
"""
public class Test {
}
""",
diagnostics: [
DiagnosticSpec(
message: "PublicInit Macro can only be applied to struct.",
line: 2,
column: 8
)
],
macros: macros
)
そして、Diagnosticのメッセージを、「PublicInit Macro can only be applied to struct.
」ではなく、
「@PublicInit can only be applied to struct.
」
に変更したいとなったら、5つのテストコードすべての変更を手作業でしなければならなく、非常に面倒です。
swift-macro-testingでは以上の3つの不便な点を解決してくれます。
swift-macro-testingを使用したテスト
swift-macro-testingではなんと!!expandedSource
を自動で生成してくれます!!
まずは以下のように書きます。
final class PublicInitMacroTests: XCTestCase {
override func invokeTest() {
withMacroTesting(
macros: ["PublicInit": PublicInitMacro.self]
) {
super.invokeTest()
}
}
func testMacro() {
assertMacro {
"""
@PublicInit
public struct Test {
let a: String
let b: () -> Void
let c: @Sendable () -> Void
}
"""
}
}
}
そしてテストを実行してみると、
assertMacro {
"""
@PublicInit
public struct Test {
let a: String
let b: () -> Void
let c: @Sendable () -> Void
}
"""
} matches: {
"""
public struct Test {
let a: String
let b: () -> Void
let c: @Sendable () -> Void
public init(
a: String,
b: @escaping () -> Void,
c: @Sendable @escaping () -> Void
) {
self.a = a
self.b = b
self.c = c
}
}
"""
}
なんと!matches:
以下がテスト実行後に自動で追加されます。 便利すぎますね。
そして、マクロに変更が加わった際は、テストケース一つ一つmatchesのクロージャを消して再実行するのではなく、
recordモードをtrueにすることで、matches
が書きかわります。
final class PublicInitMacroTests: XCTestCase {
override func invokeTest() {
withMacroTesting(
isRecording: true,
macros: ["PublicInit": PublicInitMacro.self]
) {
super.invokeTest()
}
}
...
}
これで、破壊的変更があった際にテストケースを一つ一つ直す作業がなくなりますね!!
Diagnosticのテスト
Diagnosticのテストも同じです。まずこのようなコードを書きます。
assertMacro {
"""
@PublicInit
public final class Test {}
"""
}
これが、テスト実行後...
assertMacro {
"""
@PublicInit
public final class Test {}
"""
} matches: {
"""
@PublicInit
public final class Test {}
┬────
╰─ 🛑 PublicInit Macro can only be applied to struct.
"""
}
自動生成されます。
このように、どのnodeにエラーが出ているのかがより視覚的にわかりやすくなっていて良いですね!
FixItのテスト
FixItのテストは、applyFixItsをtrueにすることで有効になります。
assertMacro(applyFixIts: true) {
"""
@PublicInit
public struct Test {
var a = true
}
"""
}
これがテスト実行後に、
assertMacro(applyFixIts: true) {
"""
@PublicInit
public struct Test {
var a = true
}
"""
} matches: {
"""
@PublicInit
public struct Test {
var a : <#Type#> = true
}
"""
}
このように自動生成されます。
従来のFixItのテストではFixItを挿入後の構文はテストできなかったので、非常に画期的ですね!!
おわりに
従来のテスト方法ではいろいろと不便な点がありましたが、swift-macro-testingを使用することで、
より視覚的で、快適なテストライフを送れそうですね!