はじめに
はじめまして。マイクロアドシステム開発部UDU(UI/UX開発ユニット)でアプリケーションエンジニアをしている崎元です。
今回は、私の所属するユニットでの自動テスト推進についてお話していきます。
まずは、お話を始めるにあたってマイクロアドシステム開発部でのテスト方針について触れさせていただきます。
目次
自動テストピラミッド
ユニットテスト、統合テスト、システムテストと分類する中、図で示すように下に行くほどテストの量が多くなっているべきという考え方になっています。
システムテストまで自動テストで行えるようにし、受け入れに自動テストのエビデンスを使用することで手動テストの割合を減らすことを目標としています。
このように変えた時に得られるメリット
- エビデンスが残しづらいケースや発生させづらいケース(外部APIを失敗させた時の動きなど)でも、自動テストで用意したテストデータ及び結果を受け入れ時にエビデンスとして使用出来る。
- 手動テストが減って自動テストが増えるので、長期的な保守で考えた場合のメンテナンスコストが減る。
- 自動テストが増えるとリファクタリングが行いやすくなり、間接的にはコード品質が上がる。
このように変えた時のデメリットは短期的な工数
- 自動テストコードの実装に慣れるまで作業工数が上がる。
- 自動テストが導入されてこなかったプロジェクトでは、導入方法検討などが大きく作業工数に影響する。
テスト自動化の対応状況
テスト自動化の対応状況ですが、ユニットテストと手動テストによって品質を担保していることが多いです。
ただし、受け入れは手動テストの結果で行っている為、自動テストが有効活用出来ていない状況です。
結合テスト自動化
現在の状況を鑑みて、まずは、今まで手を出していない結合テストの自動化を進めることとなりました。
効率的に拡充していく為の施策として、様々なパターンを自動化する前に、各APIのGET、POSTを1ケースずつ実装する方針で進めています。
簡単にAPIのGETとPOSTの結合テストを試してみましたので、それぞれの使用例をご紹介していきます。
開発環境
- フレームワーク: Spring Boot v2.3.2
- 言語: Kotlin v1.6.10
- テストフレームワーク:
- dbunit v2.7.0
- JUnit v5.6.2
- MockMVC v5.2.8
GETの結合テスト
使用例
GetUserListTest.kt
import com.github.springtestdbunit.annotation.DatabaseSetup import com.github.springtestdbunit.annotation.DatabaseSetups import com.github.springtestdbunit.annotation.DbUnitConfiguration import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import jp.microad.universe.entity.UserMaster import jp.microad.universe.security.AccountUserDetails import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.json.JacksonJsonParser import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.http.MediaType import org.springframework.security.core.GrantedAuthority import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.request.RequestPostProcessor // @SpringBootTest、@ContextConfiguration、@Import、@TestExecutionListeners、@Transactionalは省略 @AutoConfigureMockMvc @TestInstance(TestInstance.Lifecycle.PER_CLASS) @ExtendWith(SpringExtension::class) @DbUnitConfiguration(databaseConnection = ["TestDS"]) private class GetUserListTest @Autowired constructor( val mockMvc: MockMvc ) { fun getAuthority(auth: String) = mock<GrantedAuthority> { on { authority } doReturn auth } fun getMockUser(): RequestPostProcessor { val user: UserMaster = mock { on { userId } doReturn 1 on { mailAddress } doReturn "testuser@example.com" on { userPassword } doReturn "p@ssword" on { userName } doReturn "testuser" } val authorities = listOf( getAuthority("ROLE_USER"), getAuthority("ROLE_SYSTEM") ) val userDetails = mock<AccountUserDetails> { on { this.user } doReturn user on { this.authorities } doReturn authorities } return user(userDetails) } @Test @DisplayName("ユーザ一覧取得時正常ステータスが返却され、想定されたデータが返却されること") @DatabaseSetups( value = [ DatabaseSetup( value = ["/dataset/IntegrationTest/user/getUserList/setup_db.xml"], connection = "TestDS" ) ] ) fun checkGetUserList() { val expected = """ { "users": [ { "userId": 1, "userName": "hoge", "userMailAddress": "hoge@example.com" }, { "userId": 2, "userName": "fuga", "userMailAddress": "fuga@example.com" } ] } """.trimIndent() val actual = mockMvc .perform( MockMvcRequestBuilders .get("/api/userList") .with(getMockUser()) .with(SecurityMockMvcRequestPostProcessors.csrf()) .contentType(MediaType.APPLICATION_JSON) ) .andReturn() .response actual.characterEncoding = "UTF-8" Assertions.assertEquals(200, actual.status) Assertions.assertEquals( JacksonJsonParser().parseMap(expected), JacksonJsonParser().parseMap(actual.contentAsString) ) } }
setup_db.xml
<?xml version="1.0" encoding="UTF-8"?> <dataset> <user_master user_id="1" user_name="hoge" user_mail_address="hoge@example.com" user_password="p@sswordHoge" user_status="active" /> <user_master user_id="2" user_name="fuga" user_mail_address="fuga@example.com" user_password="p@sswordFuga" user_status="active" /> </dataset>
説明
DBからユーザの一覧を取得してJSON形式で返却するAPIの結合テスト例です。
ログインが必要なことを想定して、ユーザと権限をmock化しています。
HTTPレスポンスステータスが正常(200)及び、戻り値のJSONが正しいことを確認しています。
POSTの結合テスト
使用例
ChangeUserStatusTest.kt
import com.fasterxml.jackson.databind.ObjectMapper import com.github.springtestdbunit.annotation.DatabaseSetup import com.github.springtestdbunit.annotation.DatabaseSetups import com.github.springtestdbunit.annotation.DbUnitConfiguration import com.github.springtestdbunit.annotation.ExpectedDatabase import com.github.springtestdbunit.assertion.DatabaseAssertionMode import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.mock import jp.microad.universe.entity.UserMaster import jp.microad.universe.security.AccountUserDetails import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.http.MediaType import org.springframework.security.core.GrantedAuthority import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.request.RequestPostProcessor import testproject.UserEditForm // @SpringBootTest、@ContextConfiguration、@Import、@TestExecutionListeners、@Transactionalは省略 @AutoConfigureMockMvc @TestInstance(TestInstance.Lifecycle.PER_CLASS) @ExtendWith(SpringExtension::class) @DbUnitConfiguration(databaseConnection = ["TestDS"]) private class ChangeUserStatusTest @Autowired constructor( var mockMvc: MockMvc ) { fun getAuthority(auth: String) = mock<GrantedAuthority> { on { authority } doReturn auth } fun getMockUser(): RequestPostProcessor { val user: UserMaster = mock { on { userId } doReturn 1 on { mailAddress } doReturn "testuser@example.com" on { userPassword } doReturn "p@ssword" on { userName } doReturn "testuser" } val authorities = listOf( getAuthority("ROLE_USER"), getAuthority("ROLE_SYSTEM") ) val userDetails = mock<AccountUserDetails> { on { this.user } doReturn user on { this.authorities } doReturn authorities } return user(userDetails) } @Test @DisplayName("ユーザのステータスがactiveからdeletedに変更されていること") @DatabaseSetups( value = [ DatabaseSetup( value = ["/dataset/IntegrationTest/user/changeUserStatus/setup_db.xml"], connection = "TestDS" ) ] ) @ExpectedDatabase( value = "/dataset/IntegrationTest/user/changeUserStatus/expected_update_status.xml", assertionMode = DatabaseAssertionMode.NON_STRICT ) fun checkChangeStatusFromActiveToDeleted() { // リクエストパラメータをjson化 val form = UserEditForm.UserStatusEditForm("deleted") val mapper = ObjectMapper() val json = mapper.writeValueAsBytes(form) mockMvc.perform( MockMvcRequestBuilders .post("/api/userList/changeStatus?userId=1") .with(getMockUser()) .with(SecurityMockMvcRequestPostProcessors.csrf()) .content(json) .contentType(MediaType.APPLICATION_JSON) ) } }
UserEditForm.kt
// importは省略 sealed class UserEditForm { data class UserStatusEditForm( val userStatus: String ) : UserEditForm() }
setup_db.xml
<?xml version="1.0" encoding="UTF-8"?> <dataset> <user_master user_id="1" user_name="hoge" user_mail_address="hoge@example.com" user_password="p@sswordHoge" user_status="active" /> </dataset>
expected_update_status.xml
<?xml version="1.0" encoding="UTF-8"?> <dataset> <user_master user_id="1" user_name="hoge" user_mail_address="hoge@example.com" user_password="p@sswordHoge" user_status="deleted" /> </dataset>
説明
DBのuser_status項目を変更するAPIの結合テスト例です。
ログインが必要なことを想定して、ユーザと権限をmock化しています。
DBの変更結果が正しいことを確認しています。
注意事項
リクエストにcsrfトークンを設定しないと403エラーが発生します。
今後の展望
ユニットテストの拡充という課題はありますが、これからは結合テストを充実させていきたいです。
システムテストの自動化に向けてまずは一歩ずつ前進していきます。