筆者は現在、Spigot-Plugin のユニットテストに挑戦しています。ユニットテスト自体全く触ったことがなかったので基本から勉強しました。そして、とりあえずプラグインのメインクラスをテストで動かすことができたので、一旦ここで記事にしておこうと思います。
課題は JavaPlugin や Interface 系のテスト
まず、プラグインのメインクラスをインスタンス化させ、onEnable()
を実行し、期待する結果が得られるかテストをしたい。しかし、JavaPlugin を継承したクラスをそのままインスタンス化(new JavaPlugin()
)してもエラーを吐くため、その先には進めません。また、JavaPlugin の getDataFolder()
なんかも動いてくれません。なので、これらを先に解決する必要がありそうです。
解決策は Mockito にあった
上記の問題は、Mockito というライブラリで解決できそうです。海外のフォーラムをまわったり、有名どころの GitHub を眺めて見ると、いくつかのプラグインがこれでテストを書いていました。
Mockito とは
ただ、Mockito だけでは、static や private のメソッド・コンストラクタなどモックを作成するのが難しいものや、どうしても実行できないようなメソッドもあり、さらなる課題に・・・。
何でもテストできる最終兵器 PowerMock
PowerMock とは
もう、これだけでいいんじゃないかと思いながら、とりあえずこの PowerMock と Mockito を使ってテストコードを書きました。
依存関係として追加 (Gradle)
dependencies
に以下を追加して、プロジェクトを更新します。
これで、Mockito と PowerMock を使用する準備が整いました。
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile group: 'org.powermock', name: 'powermock-module-junit4', version: '1.6.5'
testCompile group: 'org.powermock', name: 'powermock-api-mockito', version: '1.6.6'
testCompile group: 'org.powermock', name: 'powermock-api-easymock', version: '1.6.6'
testCompile group: 'org.easymock', name: 'easymock', version: '3.4'
ここからは、筆者が実際に書いたコードを見ながら説明します。
実際に使ったメインクラス
Source Code : GitHub
プラグインのメインクラスをインスタンス化するために、コンストラクトを追加する必要がありました。引数に、JavaPluginLoader
PluginDescriptionFile
File
File
を受け取り、 JavaPlugin のコンストラクタにそのまま打ち込みます。このコンストラクタは protected
されていたので、セカンダリコンストラクタとして用意ました。しかし、テストのためにしか使用しないため、別の書き方を模索中です。それ以外のコードは、今回の記事には直接は関係しないです。
package com.masahirosaito.spigot.homes
import com.masahirosaito.spigot.homes.commands.HomeCommand
import com.masahirosaito.spigot.homes.homedata.HomeData
import com.masahirosaito.spigot.homes.homedata.PlayerHome
import com.masahirosaito.spigot.homes.listeners.PlayerRespawnListener
import com.masahirosaito.spigot.homes.oldhomedata.OldHomeData
import org.bukkit.plugin.PluginDescriptionFile
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.plugin.java.JavaPluginLoader
import java.io.File
class Homes : JavaPlugin {
lateinit var configs: Configs
lateinit var messenger: Messenger
lateinit var homeManager: HomeManager
lateinit var playerHomeDataFile: File
constructor() : super()
constructor(loader: JavaPluginLoader, description: PluginDescriptionFile, dataFolder: File, file: File) :
super(loader, description, dataFolder, file)
override fun onEnable() {
configs = Configs.load(File(dataFolder, "configs.json").load())
messenger = Messenger(this, configs.onDebug)
loadData()
getCommand("home").executor = HomeCommand(this)
PlayerRespawnListener(this).register()
}
override fun onDisable() {
homeManager.save(playerHomeDataFile)
}
private fun File.load(): File = this.apply {
if (!parentFile.exists()) parentFile.mkdirs()
if (!exists()) createNewFile()
}
private fun loadData() {
playerHomeDataFile = File(dataFolder, "playerhomes.json")
val oldHomeDataFile = File(dataFolder, "homedata.json")
if (!oldHomeDataFile.exists() || playerHomeDataFile.exists()) {
homeManager = HomeManager.load(playerHomeDataFile.load())
return
}
homeManager = HomeManager().apply {
OldHomeData.load(oldHomeDataFile).playerHomes.forEach {
val uuid = it.key
playerHomes.put(uuid, PlayerHome().apply {
it.value.defaultHome?.let { defaultHomeData = HomeData(uuid, "default", it) }
it.value.namedHomes.forEach { namedHomeData.add(HomeData(uuid, it.key, it.value)) }
})
}
save(playerHomeDataFile)
}
oldHomeDataFile.delete()
}
}
実際に作成したテストコード
とりあえずプラグインのメインクラスをインスタンス化させ、onEnable()
を呼び出し、最後に onDisable()
するだけのコードです。何もテストはしていません。Kotlin でモックを作ると、when
が予約語と重複するので書くのが面倒です。PowerMock の API がわかれば特につまづくところはなかったですが、@PrepareForTest
にクラス登録するのを忘れていてずっとエラーに悩まされていた時間があったので気をつけましょう。また、このコードを書くために、いくつか別のクラスも作成しました。こちら から確認できます。
package com.masahirosaito.spigot.homes
import com.masahirosaito.spigot.homes.utils.FileUtil
import com.masahirosaito.spigot.homes.utils.Util
import org.bukkit.Bukkit
import org.bukkit.Server
import org.bukkit.command.PluginCommand
import org.bukkit.plugin.Plugin
import org.bukkit.plugin.PluginDescriptionFile
import org.bukkit.plugin.PluginManager
import org.bukkit.plugin.java.JavaPluginLoader
import org.easymock.ConstructorArgs
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.api.easymock.PowerMock
import org.powermock.api.mockito.PowerMockito
import org.powermock.api.mockito.PowerMockito.*
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.reflect.Whitebox
import java.io.File
@RunWith(PowerMockRunner::class)
@PrepareForTest(Homes::class, JavaPluginLoader::class, PluginDescriptionFile::class, Server::class, PluginCommand::class)
class HomesTest {
lateinit var mockServer: Server
lateinit var homes: Homes
val pluginFolder = File("bin/test/server/plugins/homestest")
val pluginFile = File(pluginFolder, "testPluginFile")
@Before
fun setUp() {
mockServer = mock(Server::class.java).apply {
`when`(logger).thenReturn(Util.logger)
`when`(pluginManager).thenReturn(createPluginManager())
}
homes = createHomes(mockServer).apply {
`when`(name).thenReturn("Homes")
`when`(dataFolder).thenReturn(pluginFolder)
`when`(getCommand("home")).thenReturn(createPluginCommand(this))
`when`(server).thenReturn(mockServer)
}
Bukkit.setServer(mockServer)
}
@After
fun tearDown() {
Bukkit::class.java.getDeclaredField("server").let {
it.isAccessible = true
it.set(Class.forName("org.bukkit.Bukkit"), null)
}
homes.onDisable()
FileUtil.delete(pluginFolder)
}
@Test
fun onEnableTest() {
homes.onEnable()
}
fun createHomes(server: Server): Homes {
return spy(Homes(createJavaPluginLoader(server), createDescriptionFile(), pluginFolder, pluginFile))
}
fun createJavaPluginLoader(server: Server): JavaPluginLoader {
return PowerMock.createMock(JavaPluginLoader::class.java).apply {
Whitebox.setInternalState(this, "server", server)
}
}
fun createDescriptionFile(): PluginDescriptionFile {
return spy(PluginDescriptionFile("Homes", "0.6", "com.masahirosaito.spigot.homes.Homes")).apply {
`when`(commands).thenReturn(mapOf("home" to mapOf()))
`when`(authors).thenReturn(listOf())
}
}
fun createPluginCommand(homes: Homes): PluginCommand {
val constructor = PowerMock.constructor(PluginCommand::class.java, String::class.java, Plugin::class.java)
return PowerMock.createMock(PluginCommand::class.java, ConstructorArgs(constructor, "home", homes))
}
fun createPluginManager(): PluginManager {
return PowerMockito.mock(PluginManager::class.java)
}
}
最後に
筆者の 作成しているプラグイン が最近やっとバージョン 0.6 をリリースしました。まだ、正式版ではないですがこれから継続的にリリースできたらな〜って思ってます。なので、テストコードをある程度組んで CI まわして少しは楽したいです。