Upgrade to Pro — share decks privately, control downloads, hide ads and more …

NDC22 <달빛조각사>에서 서버 테스트 코드를 작성하는 방법

NDC22 <달빛조각사>에서 서버 테스트 코드를 작성하는 방법

Jongbin Oh

June 09, 2022
Tweet

More Decks by Jongbin Oh

Other Decks in Programming

Transcript

  1. Subtitles 2 <달빛조각사>에서 서버 테스트 코드를 작성하는 방법 모바일 MMORPG

    남희성 작가의 달빛조각사 소설이 원작 2019년 10월 10일 런칭 국내, 대만, 글로벌 서비스 중 서버 elixir, 클라이언트 unity
  2. Subtitles 3 <달빛조각사>에서 서버 테스트 코드를 작성하는 방법 어떻게 서버

    테스트 뿌리내렸는지 작성하는지 기반 코드를 만들었는지 더 빠르게 했는지
  3. Subtitles 4 2013~현재: XLGAMES 문명온라인, 달빛조각사 2010~2013: NC 리니지 3

    (2nd) , 리니지 이터널 2005~2010: 넥슨 코리아 마비노기, 허스키 익스프레스 NDC07 AlienBrain을 이용한 빌드, 패치 시스템 NDC12 게임 물리 엔진의 내부 동작 원리 이해 오 종 빈 게임 프로그래머 ohyecloudy.com [email protected]
  4. Subtitles 5 서버 테스트 뿌리내리기 초반 작업 세 가지 |

    테스트 성공 상태 유지 | 열심히 테스트 추가하기 | 테스트 기반 다지기 서버 테스트 작성하기 유닛 테스트와 통합 테스트는 몇 대 몇? | 통합 테스트 | 유닛 테스트 | 정리 더 짜기 쉽고 더 튼튼하게 셋업을 손쉽게 | 게임 테이블 데이터 | 패킷 순서 | sleep이 필요할 땐 병렬 테스트로 더 빠르게 순차 실행은 답이 없다 | 게임 테이블 데이터 수정이 문제 브로드캐스팅 패킷 정밀 검사 | 일부는 순차 테스트로 유지 | 결과 마무리 목 차
  5. Subtitles 7 초반 작업 세 가지 구성원들의 동의 테스트 환경

    구축 주요 로직 테스트 달빛조각사팀에 합류하기 전 완료되었던 송재경 대표님이 해결
  6. Subtitles 8 서버 테스트 뿌리내리기 테스트 기반 다지기 테스트 성공

    상태 유지 열심히 테스트 추가하기 초반 작업 세 가지
  7. Subtitles 9 실패를 방치할수록 실패 원인 찾기 비용 성공하도록 고치는

    비용 개발 프로세스 실패를 관리하는 게 목표
  8. Subtitles 10 master 브랜치 작업 브랜치 코드 포맷팅 검사 정적

    코드 분석 자동 실행 코드 리뷰 후 머지 merge 서버 테스트 자동 실행
  9. Subtitles 11 테스트 실행 master 브랜치에 머지 후 자동으로 실행

    슬랙 slack 연동 깃랩 gitlab 메일이 불친절해서 봇 제작 서버 테스트 성공 실패 여부를 쉽게 확인
  10. Subtitles 12 서버 테스트 뿌리내리기 열심히 테스트 추가하기 테스트 기반

    다지기 테스트 성공 상태 유지 초반 작업 세 가지
  11. Subtitles 14 테스트 짜는 프로그래머가 늘어남 테스트 효용성을 느낀 후

    코드 리뷰를 통해 테스트를 짜는 방법이 전파 테스트 커버리지 월드 서버 측정 시 64.5%
  12. Subtitles 16 테스트를 짜기 쉽게 한다 캐릭터 생성, 월드 입장

    등을 간단하게 테스트 코드 최적화 빠르면 빠를수록 좋다 가끔 깨지는 flaky 테스트 원인 분석 구조적 개선 진행 뒤에서 더 자세히
  13. Subtitles 18 유닛 테스트 통합 테스트 E2E 테스트 (End to

    End) 테스트 개수 테스트 스토리 작성 손쉽다 유저 행동과 유사 테스트 대상의 고립도 테스트 결과가 안정적 빠르다
  14. Subtitles 19 통합 테스트 유닛 테스트 테스트 개수 달빛 조각사에서는

    자동화된 E2E 테스트는 없다 통합 테스트 비중이 높다 전체 테스트의 90%
  15. Subtitles 21 테스트 코드의 효용성 서버 컨텐츠 자체를 테스트 버그

    재연에도 유용 디자인 변경에 유연 내부 구현 변경이 테스트에 영향을 주는 경우가 적음
  16. Subtitles 24 캐릭터마다 독립된 보관함을 가져야 한다 1. 캐릭터 보관함

    설치 2. 아이템을 생성해 보관함에 넣기 3. 같은 계정 다른 캐릭터로 접속 4. 보관함이 비어있는 걸 확인 5. 아이템을 생성해 보관함에 넣기 6. …
  17. Subtitles 25 test "캐릭터마다 독립된 보관함을 가져야 한다", %{state1: state}

    do desc("캐릭터 보관함 설치") bank_type = TableHelper.new_bank(:character) {state, _} = BankHelper.install_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}]) test "캐릭터마다 독립된 보관함을 가져야 한다", %{state1: state} do desc("캐릭터 보관함 설치") bank_type = TableHelper.new_bank(:character) {state, _} = BankHelper.install_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}]) desc("재접속했을 때, 캐릭터 보관함에 아이템이 그대로여야 한다") {state, _} = ConnectionHelper.reconnect_enter(state) BankHelper.assert_bank_items_after_load(state, bank_type, [ %{type: item_type, count: 2, locked: false} ]) desc("같은 계정 다른 캐릭터로 접속") name = ConnectionHelper.test_common_char_name(state, 1) state = put_in(state.common_char_name, name) {state, _char_id2, _} = common_character_setup(state: state) desc("다른 캐릭터로 접속했을 때, 캐릭터 보관함이 설치되어 있어야 한다") assert BankHelper.get_installed_bank_types(state) == [bank_type] desc("다른 캐릭터가 캐릭터 보관함에 저장한 아이템이 보이지 않아야 한다") assert BankHelper.assert_empty_bank(state, bank_type) desc("재접속해도 비어있는 캐릭터 보관함이 그대로여야 한다 ") {state, _} = ConnectionHelper.reconnect_enter(state)) assert BankHelper.get_installed_bank_types(state) == [bank_type] assert BankHelper.assert_empty_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}])
  18. Subtitles 26 desc("재접속했을 때, 캐릭터 보관함에 아이템이 그대로여야 한다") {state,

    _} = ConnectionHelper.reconnect_enter(state) BankHelper.assert_bank_items_after_load(state, bank_type, [ %{type: item_type, count: 2, locked: false} ]) desc("같은 계정 다른 캐릭터로 접속") name = ConnectionHelper.test_common_char_name(state, 1) state = put_in(state.common_char_name, name) {state, _char_id2, _} = common_character_setup(state: state) desc("다른 캐릭터로 접속했을 때, 캐릭터 보관함이 설치되어 있어야 한다") assert BankHelper.get_installed_bank_types(state) == [bank_type] test "캐릭터마다 독립된 보관함을 가져야 한다", %{state1: state} do desc("캐릭터 보관함 설치") bank_type = TableHelper.new_bank(:character) {state, _} = BankHelper.install_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}]) desc("재접속했을 때, 캐릭터 보관함에 아이템이 그대로여야 한다") {state, _} = ConnectionHelper.reconnect_enter(state) BankHelper.assert_bank_items_after_load(state, bank_type, [ %{type: item_type, count: 2, locked: false} ]) desc("같은 계정 다른 캐릭터로 접속") name = ConnectionHelper.test_common_char_name(state, 1) state = put_in(state.common_char_name, name) {state, _char_id2, _} = common_character_setup(state: state) desc("다른 캐릭터로 접속했을 때, 캐릭터 보관함이 설치되어 있어야 한다") assert BankHelper.get_installed_bank_types(state) == [bank_type] desc("다른 캐릭터가 캐릭터 보관함에 저장한 아이템이 보이지 않아야 한다") assert BankHelper.assert_empty_bank(state, bank_type) desc("재접속해도 비어있는 캐릭터 보관함이 그대로여야 한다 ") {state, _} = ConnectionHelper.reconnect_enter(state)) assert BankHelper.get_installed_bank_types(state) == [bank_type] assert BankHelper.assert_empty_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}])
  19. Subtitles 27 desc("다른 캐릭터가 캐릭터 보관함에 저장한 아이템이 보이지 않아야

    한다") assert BankHelper.assert_empty_bank(state, bank_type) desc("재접속해도 비어있는 캐릭터 보관함이 그대로여야 한다 ") {state, _} = ConnectionHelper.reconnect_enter(state)) assert BankHelper.get_installed_bank_types(state) == [bank_type] assert BankHelper.assert_empty_bank(state, bank_type) test "캐릭터마다 독립된 보관함을 가져야 한다", %{state1: state} do desc("캐릭터 보관함 설치") bank_type = TableHelper.new_bank(:character) {state, _} = BankHelper.install_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}]) desc("재접속했을 때, 캐릭터 보관함에 아이템이 그대로여야 한다") {state, _} = ConnectionHelper.reconnect_enter(state) BankHelper.assert_bank_items_after_load(state, bank_type, [ %{type: item_type, count: 2, locked: false} ]) desc("같은 계정 다른 캐릭터로 접속") name = ConnectionHelper.test_common_char_name(state, 1) state = put_in(state.common_char_name, name) {state, _char_id2, _} = common_character_setup(state: state) desc("다른 캐릭터로 접속했을 때, 캐릭터 보관함이 설치되어 있어야 한다") assert BankHelper.get_installed_bank_types(state) == [bank_type] desc("다른 캐릭터가 캐릭터 보관함에 저장한 아이템이 보이지 않아야 한다") assert BankHelper.assert_empty_bank(state, bank_type) desc("재접속해도 비어있는 캐릭터 보관함이 그대로여야 한다 ") {state, _} = ConnectionHelper.reconnect_enter(state)) assert BankHelper.get_installed_bank_types(state) == [bank_type] assert BankHelper.assert_empty_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}])
  20. Subtitles 28 desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id:

    item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}]) test "캐릭터마다 독립된 보관함을 가져야 한다", %{state1: state} do desc("캐릭터 보관함 설치") bank_type = TableHelper.new_bank(:character) {state, _} = BankHelper.install_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}]) desc("재접속했을 때, 캐릭터 보관함에 아이템이 그대로여야 한다") {state, _} = ConnectionHelper.reconnect_enter(state) BankHelper.assert_bank_items_after_load(state, bank_type, [ %{type: item_type, count: 2, locked: false} ]) desc("같은 계정 다른 캐릭터로 접속") name = ConnectionHelper.test_common_char_name(state, 1) state = put_in(state.common_char_name, name) {state, _char_id2, _} = common_character_setup(state: state) desc("다른 캐릭터로 접속했을 때, 캐릭터 보관함이 설치되어 있어야 한다") assert BankHelper.get_installed_bank_types(state) == [bank_type] desc("다른 캐릭터가 캐릭터 보관함에 저장한 아이템이 보이지 않아야 한다") assert BankHelper.assert_empty_bank(state, bank_type) desc("재접속해도 비어있는 캐릭터 보관함이 그대로여야 한다 ") {state, _} = ConnectionHelper.reconnect_enter(state)) assert BankHelper.get_installed_bank_types(state) == [bank_type] assert BankHelper.assert_empty_bank(state, bank_type) desc("캐릭터 보관함에 넣을 수 있는 아이템을 만든다") %{item_db_id: item_db_id, item_type: item_type} = BankHelper.create_some_storable_item_get_item(state, bank_type, 2) desc("아이템을 보관하고 확인한다") BankHelper.store_items(state, bank_type, %{id: item_db_id, count: 2}) InventoryHelper.assert_remain_items(state, %{}, %{item_db_id => 2}) BankHelper.assert_bank_items(state, bank_type, [%{type: item_type, count: 2}])
  21. Subtitles 29 존 이동 버그 재현 테스트 예제 버그 재현에도

    유용 클라이언트처럼 패킷을 주고 받는다 간혹 존 이동이 실패해 이전 존으로 이동한다는 제보 발생 빈도가 낮다 재현 조건은 모르는 상태
  22. Subtitles 30 @tag setup_char_count: 2 test "reserve channel except channels

    being destroyed", %{state1: state1, state2: state2} do desc("채널에 사람이 없으면 바로 파괴하게 해서 파괴와 동시에 캐릭터 입장을 시뮬레이션") TestTable.scoped_modify_const(:PRESERVE_TIME, 0) TestTable.scoped_modify_const(:MOVER_PRESERVE_TIME, 0) @tag setup_char_count: 2 test "reserve channel except channels being destroyed", %{state1: state1, state2: state2} do desc("채널에 사람이 없으면 바로 파괴하게 해서 파괴와 동시에 캐릭터 입장을 시뮬레이션") TestTable.scoped_modify_const(:PRESERVE_TIME, 0) TestTable.scoped_modify_const(:MOVER_PRESERVE_TIME, 0) desc("두 캐릭터가 서로 다른 두 지역을 동시에 왔다갔다 한다") desc("채널 파괴 타이밍과 입장 타이밍을 근접하게 한다") {Q.const_zone_south_serabourg(), Q.const_zone_serabourg()} # 첫번째 요소와 두번째 요소 위치를 바꾸는 걸 반복한다 |> Stream.iterate(fn {z1, z2} -> {z2, z1} end) # 100번 정도 시도해본다 |> Enum.take(100) |> Enum.reduce({state1, state2}, fn {z1, z2}, {state1, state2} -> # 동시에 포탈로 존을 이동한다 [ Task.async(fn -> TeleportHelper.portal(state1, z1) |> elem(0) end), Task.async(fn -> TeleportHelper.portal(state2, z2) |> elem(0) end) ] |> Enum.map(&Task.await/1) |> List.to_tuple() end) end
  23. Subtitles 31 desc("두 캐릭터가 서로 다른 두 지역을 동시에 왔다갔다

    한다") desc("채널 파괴 타이밍과 입장 타이밍을 근접하게 한다") {Q.const_zone_south_serabourg(), Q.const_zone_serabourg()} # 첫번째 요소와 두번째 요소 위치를 바꾸는 걸 반복한다 |> Stream.iterate(fn {z1, z2} -> {z2, z1} end) # 100번 정도 시도해본다 |> Enum.take(100) @tag setup_char_count: 2 test "reserve channel except channels being destroyed", %{state1: state1, state2: state2} do desc("채널에 사람이 없으면 바로 파괴하게 해서 파괴와 동시에 캐릭터 입장을 시뮬레이션") TestTable.scoped_modify_const(:PRESERVE_TIME, 0) TestTable.scoped_modify_const(:MOVER_PRESERVE_TIME, 0) desc("두 캐릭터가 서로 다른 두 지역을 동시에 왔다갔다 한다") desc("채널 파괴 타이밍과 입장 타이밍을 근접하게 한다") {Q.const_zone_south_serabourg(), Q.const_zone_serabourg()} # 첫번째 요소와 두번째 요소 위치를 바꾸는 걸 반복한다 |> Stream.iterate(fn {z1, z2} -> {z2, z1} end) # 100번 정도 시도해본다 |> Enum.take(100) |> Enum.reduce({state1, state2}, fn {z1, z2}, {state1, state2} -> # 동시에 포탈로 존을 이동한다 [ Task.async(fn -> TeleportHelper.portal(state1, z1) |> elem(0) end), Task.async(fn -> TeleportHelper.portal(state2, z2) |> elem(0) end) ] |> Enum.map(&Task.await/1) |> List.to_tuple() end) end
  24. Subtitles 32 |> Enum.reduce({state1, state2}, fn {z1, z2}, {state1, state2}

    -> # 동시에 포탈로 존을 이동한다 [ Task.async(fn -> TeleportHelper.portal(state1, z1) |> elem(0) end), Task.async(fn -> TeleportHelper.portal(state2, z2) |> elem(0) end) ] |> Enum.map(&Task.await/1) |> List.to_tuple() end) end @tag setup_char_count: 2 test "reserve channel except channels being destroyed", %{state1: state1, state2: state2} do desc("채널에 사람이 없으면 바로 파괴하게 해서 파괴와 동시에 캐릭터 입장을 시뮬레이션") TestTable.scoped_modify_const(:PRESERVE_TIME, 0) TestTable.scoped_modify_const(:MOVER_PRESERVE_TIME, 0) desc("두 캐릭터가 서로 다른 두 지역을 동시에 왔다갔다 한다") desc("채널 파괴 타이밍과 입장 타이밍을 근접하게 한다") {Q.const_zone_south_serabourg(), Q.const_zone_serabourg()} # 첫번째 요소와 두번째 요소 위치를 바꾸는 걸 반복한다 |> Stream.iterate(fn {z1, z2} -> {z2, z1} end) # 100번 정도 시도해본다 |> Enum.take(100) |> Enum.reduce({state1, state2}, fn {z1, z2}, {state1, state2} -> # 동시에 포탈로 존을 이동한다 [ Task.async(fn -> TeleportHelper.portal(state1, z1) |> elem(0) end), Task.async(fn -> TeleportHelper.portal(state2, z2) |> elem(0) end) ] |> Enum.map(&Task.await/1) |> List.to_tuple() end) end
  25. Subtitles 34 다양한 케이스에 대한 테스트가 필요할 때 통합 테스트의

    복잡한 준비 과정 X 테스트 대상 알고리즘 기반 유틸 함수
  26. Subtitles 35 요리 재료로 적절한 레시피 찾기 테스트 예제 다양한

    재료와 레시피로 테스트해야 한다 통합 테스트로 하려면 다 생성해야 함 레시피를 찾는 함수 격리 테스트가 가능 유닛 테스트로 인자만 바꿔서 테스트할 수 있다
  27. Subtitles 36 test "요리 재료 종류가 일치해야 한다. 모자라거나 초과하는

    레시피를 선택하면 안 된다" do pick = build_pick_optimal_recipe_ids_partial_func( [ %{:craft_id => 1, :materials => %{1 => 5}}, %{:craft_id => 2, :materials => %{1 => 5, 2 => 2}}, %{:craft_id => 3, :materials => %{1 => 5, {:meat, -1} => 1, 3 => 1}} ] ) assert pick.(%{1 => 5, 2 => 5}) == [2], "모자라거나 넘치는 1, 3 레시피를 선택하면 안 된다“ assert pick.(%{1 => 5, 2 => 5, 3 => 4, 4 => 3}) == [3], "모자라는 1, 2 레시피를 선택하면 안 된다" end 레시피를 찾을 때 사용하는 필드만 정의 아이템 타입에 해당하는 값을 넘겨서 테스트
  28. Subtitles 37 간단한 함수 테스트 몇 줄 안 되는데, 별도

    테스트 파일에 정의해야 하나? 함수 설명에 적으면 사용 예제로 참고할 수 있는데
  29. Subtitles 38 @doc """ 다른 타입 아이템 리스트를 받아 각

    type별 count를 계산한다. {type, count} 튜플 리스트를 리턴한다. iex> ItemUtil.to_type_count_tuples([]) [] iex> ItemUtil.to_type_count_tuples([ ...> %Item{type: 1, count: 1}, ...> %Item{type: 2, count: 10}, ...> %BankItem{type: 1, count: 2}, ...> {2, {:none, 0, 0}, 3}]) [{1, 3}, {2, 13}] """ def to_type_count_tuples(items) do # 멋진 구현 end 함수 설명 실제로 테스트가 된다
  30. Subtitles 39 공용 유틸 함수를 doctest 동작이 검증된 함수 사용

    예제 호출 빈도가 높은 유틸 함수 이해에 도움
  31. Subtitles 41 통합 테스트 비중이 높다 유닛 테스트도 하긴 한다

    통합 테스트: 서버 동작, 게임 컨텐츠 유닛 테스트: 알고리즘, 공용 유틸 함수
  32. Subtitles 43 @tag setup_char_count: 2 @tag setup_char_class: Q.char_class_mage() @tag setup_char_zone:

    Q.const_zone_test_zoo() @tag setup_char_makeup: char_makeup_female() @tag make_party: true test "끝내주는 테스트", %{state1: state, state2: state2} do # 테스트 코드 end 세팅할 캐릭터 수, 직업, 성별 등을 설정 테스트 코드 작성에 집중할 수 있고 테스트 코드 가독성도 좋아진다.
  33. Subtitles 44 더 짜기 쉽고 더 튼튼하게 sleep이 필요할 땐

    셋업을 손쉽게 게임 테이블 데이터 패킷 순서
  34. Subtitles 45 게임 테이블 데이터를 그대로 사용 매번 테스트할 조건에

    맞는 데이터 찾기 찾는 게 귀찮아서 특정 데이터가 반복적으로 사용됨 기획자가 데이터를 변경해서 테스트가 실패하기도
  35. Subtitles 46 @item_type 123123 # 보관함에 보관할 수 있는 아이템

    test "보관함 기본 동작", %{state1: state} do {state, %{bank_item_db_id: bank_item_db_id}} = BankHelper.install_bank(state, bank_type) desc("아이템을 보관한다") %{item_db_id: item_db_id} = ItemHelper.put_item(state, @item_type, 2) BankHelper.store_items(state, [ %{ bank_type: bank_type, request_items: [%{id: item_db_id, count: 1}] } ]) #... end
  36. Subtitles 47 게임 테이블 데이터를 수정해서 사용 해당 테스트가 끝나면

    원래 속성으로 복구 한땀한땀 전체 속성 정의는 필요 X 기획자가 테스트를 깨뜨리지 않는다 데이터 조작이 쉬워지니 다양한 속성을 테스트
  37. Subtitles 48 @item_type 123123 # 보관함에 보관할 수 있는 아이템

    test "보관함 기본 동작", %{state1: state} do # 테스트에 필요한 속성을 정의해서 기획자가 용도를 바꾸더라도 테스트가 깨지지 않게 설정 TestTable.scoped_modify_table(:item_table, @item_type, fn old -> Map.put(old, "보관함_보관_여부", true) end) desc("아이템을 보관한다") %{item_db_id: item_db_id} = ItemHelper.put_item(state, @item_type, 2) # 멋진 테스트 end
  38. Subtitles 49 더 짜기 쉽고 더 튼튼하게 셋업을 손쉽게 게임

    테이블 데이터 패킷 순서 sleep이 필요할 땐
  39. Subtitles 50 # 몬스터가 플레이어에게 dot 데미지를 주는 버프를 걸고

    때린다 # 공격 데미지가 들어온다 assert_packet_received(state, Q.sc_attack()) # dot 데미지가 들어온다 assert_packet_received(state, Q.sc_dot_damage()) 성공할 때도 있고 실패할 때도 있다 공격 패킷이 빨리 가면 성공 늦게 가면 실패
  40. Subtitles 51 # 몬스터가 플레이어에게 dot 데미지를 주는 버프를 걸고

    때린다 assert_packets_received(state, 2, fn {Q.sc_attack(), _field1, _field2} -> # 공격 데미지가 들어온다 :ok {Q.sc_dot_damage(), _field1, _field2, _field3} -> # dot 데미지가 들어온다 :ok _ -> :ignore end) pattern matching 사용 순서에 상관없이 패킷이 다 들어오면 성공 하지만 조합이 쉽지 않다
  41. Subtitles 52 조합할 수 있는 패킷 매처 구현 순서 상관없이

    확인할 패킷을 리스트로 구성할 수 있다
  42. Subtitles 53 테스트 헬퍼 함수 디자인에 도움 패킷 매처를 인자로

    받아 추가적인 패킷 처리 가능 몬스터 kill 함수를 만들었는데 업적 달성, 레벨 업 패킷을 추가 처리하려면?
  43. Subtitles 54 CombatHelper.kill( state, monster_id, [ PacketMatcherFactory.sc_achievement(57, count: 1, nth:

    0, done: false), PacketMatcherFactory.sc_achievement(2, count: 1, nth: 0, done: false) ] ) CombatHelper.kill( state, monster_id, PacketMatcherFactory.sc_class_level_up(exp: @gain_exp) ) 몬스터를 죽였을 때, 업적 달성 몬스터를 죽이고 경험치 획득 몬스터가 죽었을 때 받는 패킷은 함수 내부에서 처리
  44. Subtitles 56 sleep 시간 하드코딩의 문제점 넉넉한 sleep이 테스트를 느리게

    한다 간당간당한 sleep이 테스트를 깨지기 쉽게 한다 # 아이템을 사용 후 db 에서도 삭제됐는지 확인 # 저장하는데 텀이 있어서 약간 기다렸다가 확인한다 Process.sleep(1000) get_db_item_count(state, item_type) == 0
  45. Subtitles 57 간격을 두고 여러 번 검사 200ms 간격으로 10번

    검사 모두 실패해야 실패 더 빠르고 튼튼한 테스트가 가능 # 아이템을 사용 후 db 에서도 삭제됐는지 확인 eventually( fn -> get_db_item_count(state, item_type) == 0 end, count: 10, interval: 200 )
  46. Subtitles 58 병렬 테스트로 더 빠르게 브로드캐스팅 패킷 정밀 검사

    순차 실행은 답이 없다 게임 테이블 데이터 수정이 문제
  47. Subtitles 59 점점 오래 걸리는 테스트 시간 총 1376개 테스트,

    총 3955s(65분) 테스트 개별 소요 시간을 줄이는 데는 한계가 있다 동시에 여러 서버 테스트를 실행해야 한다 이렇게 테스트가 많아질 거라 생각을 못 했다
  48. Subtitles 60 월드 서버 테스트 코드 A 패킷 테스트 코드

    B 테스트 코드 A가 끝나면 시작 테스트 순차 실행 - 기존 방식
  49. Subtitles 62 병렬 테스트로 더 빠르게 일부는 순차 테스트로 유지

    순차 실행은 답이 없다 게임 테이블 데이터 수정이 문제 브로드캐스팅 패킷 정밀 검사
  50. Subtitles 64 수정하지 말고 추가 type id를 새로 발급해서 다른

    테스트에 영향 X 데이터의 모든 속성을 정의하는 건 괴롭다 수정 확률이 낮은 옛날 데이터를 복사 테스트에 필요한 속성을 덮어쓴다
  51. Subtitles 65 TableHelper.new_item(:scroll, count: 1, attrs: [bahsra_time: [abs: @test_time]], effects:

    [:bahsra_time] ) TableHelper.new_achievement(:get_item_kind_grade, {:potion, Q.item_grade_type_poor()}) TableHelper.new_drop_pack( %{ TableHelper.new_drop_pack_class_gender() => [ TableHelper.new_drop_pack_item(item_type1, min_count: 2, max_count: 2), TableHelper.new_drop_pack_item(item_type1, min_count: 3, max_count: 3), TableHelper.new_drop_pack_item(item_type2, min_count: 4, max_count: 4) ] }, method: :each)
  52. Subtitles 66 병렬 테스트로 더 빠르게 결과 순차 실행은 답이

    없다 게임 테이블 데이터 수정이 문제 브로드캐스팅 패킷 정밀 검사 일부는 순차 테스트로 유지
  53. Subtitles 67 현재 테스트 중인 캐릭터가 대상인지 검사 같은 지역에서

    실행 중인 다른 테스트 영향일 수 있다 attack 패킷의 damage 필드를 바로 검사 X 공격자, 피격자를 확인 후에 검사 다른 테스트에서 공격해 방송한 패킷일 수 있다
  54. Subtitles 68 병렬 테스트로 더 빠르게 게임 테이블 데이터 수정이

    문제 브로드캐스팅 패킷 정밀 검사 일부는 순차 테스트로 유지 결과
  55. Subtitles 69 병렬 X - 게임 상수를 변경하는 테스트 주로

    시간, 기간에 관련된 상수 다른 테스트에 영향을 줄 수 있어서 순차 실행 # 캐릭터 삭제 대기 기간을 1초로 변경함 TestTable.scoped_modify_const(:DELAYED_DELETE_CHAR_DELAY, 1) TestTable.scoped_modify_const(:SERVER_INTEGRATION_DELETE_CHAR_DELAY, 1) # 캐릭터 삭제 테스트 ...
  56. Subtitles 70 병렬 X - 컨텐츠 스위치 사용 테스트 실시간으로

    특정 컨텐츠를 활성화/비활성화 다른 테스트에 영향을 줄 수 있어서 순차 실행 # item_type을 팔 수 없게 설정한다 Q5ContentSwitch.scoped_set_item_restriction(item_type, Q.item_restriction_type_no_trade(), true) # 팔 수 없는 아이템을 파는 테스트... # 남의 집 방문 컨텐츠를 off한다 ContentSwitchHelper.scoped_content_switch(Q.content_switch_element_visit_house(), false) # 남의 집에 방문할 수 있는지 테스트...
  57. Subtitles 72 효과가 있다 3955s(65분) -> 1826s(30분) 아직 진행중 순차

    테스트를 염두에 두고 짠 테스트가 많다
  58. Subtitles 74 서버 테스트 뿌리내리기 초반 작업 세 가지 |

    테스트 성공 상태 유지 | 열심히 테스트 추가하기 | 테스트 기반 다지기 서버 테스트 작성하기 유닛 테스트와 통합 테스트는 몇 대 몇? | 통합 테스트 | 유닛 테스트 | 정리 더 짜기 쉽고 더 튼튼하게 셋업을 손쉽게 | 게임 테이블 데이터 | 패킷 순서 | sleep이 필요할 땐 병렬 테스트로 더 빠르게 순차 실행은 답이 없다 | 게임 테이블 데이터 수정이 문제 브로드캐스팅 패킷 정밀 검사 | 일부는 순차 테스트로 유지 | 결과 마무리 목 차
  59. Subtitles 75 남은 일 서버 테스트를 master 브랜치에 머지 merge

    하기 전에 실행 10분대 테스트 시간과 안정성이 확보돼야 한다 master 브랜치 작업 브랜치 코드 포맷팅 검사 정적 코드 분석 자동 실행 코드 리뷰 후 머지 merge
  60. Subtitles 76 테스트를 짜는 게 막막하다면 스토리 작성에 유리한 통합

    테스트부터 시작부터 이렇게 게임 테이블 데이터는 추가 병렬로 테스트를 실행
  61. Subtitles 77 Designing Elixir Systems with OTP James Edward Gray

    II, Bruce A. Tate Testing Elixir Andrea Leopardi, Jeffrey Matthias Google Testing Blog https://testing.googleblog.com/ The Elixir programming language https://elixir-lang.org/ feature branch workflow - atlassian.com https://bit.ly/3KSgplk slab - slack bot http://ohyecloudy.com/pnotes/archives/side-project-slab/ 참 고