
๐ Unity Script vs Test Script
ํญ๋ชฉ | ์ผ๋ฐ์ ์ธ Unity Script (MonoBehaviour) | Test Script (NUnit) |
๋์ ๋ฐฉ์ | GameObject์ Component๋ก ์ถ๊ฐ๋์ด์ผ ์คํ๋จ | Test Runner๊ฐ ์๋์ผ๋ก ์คํ |
์คํ ์์ | Unity์ Start(), Update() ๋ฑ์์ ํธ์ถ๋จ | Test Runner๊ฐ SetUp(), Test() ๋ฑ์ ํธ์ถ |
Unity Scene ํ์ ์ฌ๋ถ | ํ์ O (Scene์์ ์คํ๋จ) | ํ์ X (GameObject ์์ด๋ ๋์ ๊ฐ๋ฅ) |
+ ์ถ๊ฐ ์ง์
Test Runner
~> Unity์์ ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ ๋๋ Test Runner ๋๊ตฌ ์ฌ์ฉ
~> [ Window ] > [ General ] > [ Test Runner ] ์์ ์คํ ๊ฐ๋ฅ
๐ Unity Test Framework๋?
Unity Test Framework๋ Unity์์ ์๋ํ๋ ํ ์คํธ๋ฅผ ์ํํ ์ ์๋๋ก ์ง์ํ๋ NUnit ๊ธฐ๋ฐ์ ํ ์คํธ ํ๋ ์์ํฌ์ด๋ค. ์ด๋ฅผ ํตํด ์ผ๋ฐ์ ์ธ ๋จ์ ํ ์คํธ๋ถํฐ ํตํฉ ํ ์คํธ, ์ฑ๋ฅ ํ ์คํธ๊น์ง ๊ฐ๋ฅํ๋ค. ์ฆ, ๋จ์ํ ๊ฐ๋ณ ํจ์ ๊ฒ์ฆ๋ถํฐ, ๊ฒ์์ UI, ๋ฌผ๋ฆฌ ์์ง, ๋คํธ์ํฌ, ์ฑ๋ฅ ์ต์ ํ ํ ์คํธ๊น์ง ์๋ํํ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ์ด๋ค.
[ Uniity Test Framwork์ ํน์ง ]
โ NUnit ๊ธฐ๋ฐ
~> Assert ๋ฌธ๋ฒ ์ง์ (AreEqual(), IsTrue(), IsNull() ๋ฑ)
~> SetUp(), TearDown()์ ํ์ฉํ ํ ์คํธ ํ๊ฒฝ ์ค์
โ UnityTest() ๋ฅผ ํ์ฉํ MonoBehaviour ํ ์คํธ
~> ์ผ๋ฐ์ ์ธ Test()๋ MonoBehaviour ํ ์คํธ ๋ถ๊ฐ๋ฅ
~> UnityTest()๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋ฃจํด ๊ธฐ๋ฐ ํ ์คํธ ๊ฐ๋ฅํ์ฌ, ํ๋ ์ ๋จ์ ๋์ ๊ฒ์ฆ ๊ฐ๋ฅ
โ Edit Mode vs Play Mode
~> Edit Mode๋ MonoBehaviour ์์ด ๋น ๋ฅธ ๋จ์ ํ ์คํธ ์คํ
~> Play Mode๋ MonoBehaviour๋ฅผ ํฌํจํ Physics, UI, Animation, Rigidbody ํ ์คํธ ๊ฐ๋ฅ
โ CI/CD ์ฐ๋ ๊ฐ๋ฅ
~> Unity CLI(Command Line Interface)๋ฅผ ํ์ฉํ์ฌ ํ ์คํธ ์๋ ์คํ ๊ฐ๋ฅ
~> GitHub Actions, GitLab CI/CD, Jenkins์ ์ฐ๋ํ์ฌ ๋น๋ ์์ ์ฑ ํ๋ณด ๊ฐ๋ฅ
[ ์ฌ์ฉ ๋ฐฉ๋ฒ ]
โญ ๊ณต์ ๋ฌธ์ ๋ฐ๋ก๊ฐ๊ธฐ
๐ก Test Framework ํจํค์ง๋ Unity 2019.2 ์ดํ ๋ฒ์ ๋ถํฐ ๊ธฐ๋ณธ์ ์ผ๋ก ํฌํจ๋์ด ์์ผ๋ฏ๋ก, using NUnit.Framework;๋ฅผ ํตํด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
1๏ธโฃ Assembly Definition ํ์ผ ์์ฑ
~> ํ ์คํธ ํ์ผ๋ค์ด ํฌํจ๋ ํด๋ ๋ด์์ [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] > [ Create ] > [ Scripting ] > [ Assembly Definition ]
2๏ธโฃ .asmdef ์์ ์ฐธ์กฐ ์ถ๊ฐ
~> ์์ฑ๋ .asmdef ํ์ผ์ ์ ํ
~> Inspector ์ฐฝ์์ [ Assembly Definition References ] ์น์ ์ UnityEngine.TestRunner์ UnityEditor.TestRunner ์ถ๊ฐ
๐ Unity Performance Testing Extension์ด๋?
Unity Performance Testing Extension์ด๋ Unity์์ ์ฑ๋ฅ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์คํํ๊ณ ์ฑ๋ฅ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ ์ ์๋๋ก ์ง์ํ๋ ํ์ฅ ๊ธฐ๋ฅ์ด๋ค. ์ด๋ฅผ ํตํด ํ๋ ์ ์๋(FPS), CPU/GPU ์ฌ์ฉ๋, ๋ฉ๋ชจ๋ฆฌ ํ ๋น๋, ์ฝ๋ ์คํ ์๋ ๋ฑ์ ์ธก์ ํ์ฌ ๊ฒ์์ ์ฑ๋ฅ์ ์ต์ ํํ ์ ์๋ค. ์ฆ, ํน์ ๊ธฐ๋ฅ์ ์คํ ์๊ฐ์ ๋ถ์ํ๊ณ , ์ฝ๋ ๋ณ๊ฒฝ ์ ํ ์ฑ๋ฅ์ ๋น๊ตํ์ฌ ์ต์ ํ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ ์ ์๋ ๊ฐ๋ ฅํ ์ฑ๋ฅ ํ ์คํธ ๋๊ตฌ์ด๋ค.
[ Unity Performance Testing Extension์ ํน์ง ]
โ ์ฝ๋ ์คํ ์๊ฐ ์ธก์
~> Measure.Method()๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ์ฝ๋ ๋ธ๋ก์ ์คํ ์๋ ๋ถ์ ๊ฐ๋ฅ
~> WarmupCount(), MeasurementCount()๋ฅผ ํ์ฉํด ํ๊ท ๊ฐ ๋์ถ ๊ฐ๋ฅ
โ FPS ๋ฐ CPU/GPU ์ฑ๋ฅ ์ธก์
~> Measure.Frames()๋ฅผ ์ฌ์ฉํ์ฌ FPS ๋ฐ CPU/GPU ์ฌ์ฉ๋์ ์๋์ผ๋ก ๋ถ์ ๊ฐ๋ฅ
~> ํน์ ๊ตฌ๊ฐ์์ ํ๋ ์ ์ฑ๋ฅ์ ์ธก์ ํ์ฌ ์ต์ ํ ์ฌ๋ถ ํ๋จ ๊ฐ๋ฅ
โ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ธก์
~> Measure.Profiler()๋ฅผ ํ์ฉํ์ฌ Garbage Collection(GC) ๋ฐ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ถ์ ๊ฐ๋ฅ
~> ๋ถํ์ํ ๋ฉ๋ชจ๋ฆฌ ํ ๋น์ ๊ฐ์งํ์ฌ ์ฑ๋ฅ ์ต์ ํ ๊ฐ๋ฅ
โ ์ฑ๋ฅ ๊ธฐ์ค ์ค์ ๋ฐ ๋น๊ต ํ ์คํธ ๊ฐ๋ฅ
~> ShouldBeUnder()๋ฅผ ํ์ฉํ์ฌ ํน์ ์ฝ๋์ ์คํ ์๊ฐ์ด ๋ชฉํ ๊ธฐ์ค๋ณด๋ค ์ค๋ ๊ฑธ๋ฆด ๊ฒฝ์ฐ ํ ์คํธ ์คํจ ์ฒ๋ฆฌ
~> ์ฝ๋ ๋ณ๊ฒฝ ์ ํ์ ์ฑ๋ฅ์ ๋น๊ตํ์ฌ ์ต์ ํ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆ ๊ฐ๋ฅ
โ CI/CD ์ฐ๋ ๊ฐ๋ฅ
~> Unity CLI(Command Line Interface)๋ฅผ ํ์ฉํ์ฌ ์ฑ๋ฅ ํ ์คํธ ์๋ ์คํ ๊ฐ๋ฅ
~> GitHub Actions, GitLab CI/CD, Jenkins์ ์ฐ๋ํ์ฌ ์ฑ๋ฅ ์ ํ ๊ฐ์ง ๊ฐ๋ฅ
[ ์ฌ์ฉ ๋ฐฉ๋ฒ ]
โญ ๊ณต์ ๋ฌธ์ ๋ฐ๋ก๊ฐ๊ธฐ
๐ก Performance testing API ํจํค์ง๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํฌํจ๋์ด ์์ง ์์ผ๋ฏ๋ก, ๋ณ๋์ ์ค์น ํ using Unity.PerformanceTesting; ๋ฅผ ํตํด ์ฌ์ฉ ๊ฐ๋ฅ
1๏ธโฃ ํจํค์ง ์ค์น
~> [ Window ] > [ Package Manager ] ์ด๊ธฐ
~> Unity Registry ํญ์์ Performance testing API ๊ฒ์ ํ [ Install ]
2๏ธโฃ Assembly Definition ํ์ผ ์์ฑ
~> ํ ์คํธ ํ์ผ๋ค์ด ํฌํจ๋ ํด๋ ๋ด์์ [ ์ค๋ฅธ์ชฝ ๋ง์ฐ์ค ] > [ Create ] > [ Scripting ] > [ Assembly Definition ]
3๏ธโฃ .asmdef ์์ ์ฐธ์กฐ ์ถ๊ฐ
~> ์์ฑ๋ .asmdef ํ์ผ์ ์ ํ
~> Inspector ์ฐฝ์์ [ Assembly Definition References ] ์น์ ์ Unity.PerformanceTesting ์ถ๊ฐ
๐ Assembly Definition ํ์ผ์ด๋?
.asmdef (Assembly Definition) ํ์ผ์ Unity์ ์คํฌ๋ฆฝํธ๋ฅผ ํน์ ๊ทธ๋ฃน์ผ๋ก ๋๋์ด ๊ด๋ฆฌํ๋ ์ค์ ํ์ผ์ด๋ค. ๋ง์ฝ .asmdef ํ์ผ์ด ์์ผ๋ฉด, Unity๋ ๋ชจ๋ C# ์คํฌ๋ฆฝํธ๋ฅผ ํ ๋ฒ์ ์ปดํ์ผํ๋ฉฐ, ์ด๋ก ์ธํด ๋ถํ์ํ ์ฝ๋๊น์ง ํฌํจ๋์ด ๋น๋ ์๊ฐ์ด ๊ธธ์ด์ง๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด .asmdef ํ์ผ์ ํ์ฉํ๋ฉด ์ปดํ์ผ ์๋๋ฅผ ์ต์ ํํ๊ณ , ์คํ ์ฝ๋์ ํ ์คํธ ์ฝ๋๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๋ค.
[ Assets ๊ตฌ์ฑ ]
๐ Scripts
ใด ๐ PlayerController.cs
ใด ๐ CameraController.cs
ใด ๐ Runtime.asmdef
ใด ๐ Tests
ใด ๐ PlayerControllerTest.cs
ใด ๐ CameraControllerTest.cs
ใด ๐ Tests.asmdef
์๋ฅผ ๋ค์ด, ์ Assets ๊ตฌ์ฑ์์ Unity๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ ์คํฌ๋ฆฝํธ๋ฅผ ํ ๋ฒ์ ์ปดํ์ผํ๋ค. ์ด๋ก ์ธํด ํ ์คํธ ์ฝ๋(Tests/)์ ์คํ ์ฝ๋(Scripts/)๊ฐ ๋ถ๋ฆฌ๋์ง ์๊ณ ํจ๊ป ๋น๋๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๊ฐ๊ฐ์ ํด๋์ .asmdef ํ์ผ์ ์์ฑํ์ฌ ์คํ ์ฝ๋์ ํ ์คํธ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํ๋ค. ์ฆ, Runtime.asmdef๋ ์ค์ ๊ฒ์ ์คํ์ ์ํ ์ฝ๋๋ง ํฌํจ๋๋ฉฐ, ํ ์คํธ ์ฝ๋์ ๋ถ๋ฆฌ๋๋ฏ๋ก, ๋น๋ ์ ํ ์คํธ ์ฝ๋๊ฐ ํฌํจ๋์ง ์์ ์คํ ์ฝ๋๊ฐ ๊น๋ํ๊ฒ ์ ์ง๋๋ค.
์์ ๊ฐ์ด .asmdef ํ์ผ์ ์ค์ ํ๋ฉด, ํ ์คํธ ์ฝ๋(Tests/ ํด๋ ์์ ํ ์คํธ ์ฝ๋)๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์คํ ์ฝ๋(PlayerController.cs ๋ฑ)์ ๋ฐ๋ก ์ ๊ทผํ ์ ์๋ค. ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด Tests.asmdef์์ Runtime.asmdef๋ฅผ ์ฐธ์กฐํด์ผ ํ๋ค. ์ฆ, Tests.asmdef ํ์ผ์ [ Assembly Definition References ] ํญ๋ชฉ์ Runtime.asmdef๋ฅผ ์ถ๊ฐํ๋ฉด, ํ ์คํธ ์ฝ๋์์ ์คํ ์ฝ๋(PlayerController.cs, CameraController.cs)๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค.
๐ ํ ์คํธ ํ๊ฒฝ
๋ณธ์ธ์ ํ ์คํธ ์๋ํ๋ฅผ ์ํด, ๊ธฐ๋ณธ์ ์ธ ์บ๋ฆญํฐ ์ด๋ ๋ฐ ์ ๋๋งค์ด์ , ์นด๋ฉ๋ผ ๊ณ ์ ๊ธฐ๋ฅ๋ง ๊ตฌํ๋ ํ๋ก์ ํธ๋ฅผ ์ฌ์ฉํ์๋ค.
[ ํ ์คํธ ๋์ ]
โข Unity ๋ฒ์ : Unity 6 (6000.0.38f1)
โข ํ ์คํธ ๋์ Script
~> PlayerController.cs (์บ๋ฆญํฐ ์ด๋ ๋ฐ ์ ๋๋ฉ์ด์ )
~> CameraController.cs (์นด๋ฉ๋ผ ๊ณ ์ ๊ธฐ๋ฅ)
[ ํ ์คํธ ํจํค์ง ์ค์น ]
โข Test Framework (๊ธฐ๋ณธ ํฌํจ ์ฌ๋ถ ํ์ธ ํ ํ์ ์ ์ค์น)
โข Performance Testing API (Package Manager์์ ์ค์น)
[ Assets ๊ตฌ์ฑ ]
๐ Scripts
ใด ๐ PlayerController.cs
ใด ๐ CameraController.cs
ใด ๐ Runtime.asmdef
ใด ๐ Tests
ใด ๐ PlayerControllerTest.cs
ใด ๐ CameraControllerTest.cs
ใด ๐ Tests.asmdef
[ .asmdef ํ์ผ ์ค์ ]
โข Tests.asmdef ํ์ผ์ [ Assembly Definition References ]
~> UnityEngine.TestRunner.asmdef
~> UnityEditor.TestRunner.asmdef
~> Unity.PerformanceTesting.asmdef
~> Runtime.asmdef
UnityEngine.TestRunner๋ ํ ์คํธ ์คํ์ ์ํด, UnityEditor.TestRunner๋ Unity Editor์์ ํ ์คํธ๋ฅผ ์คํํ๊ธฐ ์ํด, Unity.PerformanceTesting์ ์ฑ๋ฅ ํ ์คํธ๋ฅผ ์ํด, Runtime.asmdef๋ ํ ์คํธ ์ฝ๋์์ ์คํ ์ฝ๋(PlayerController.cs, CameraController.cs)์ ์ ๊ทผํ ์ ์๋๋ก ํ๊ธฐ ์ํด ์ถ๊ฐํด์ค ๊ฒ์ด๋ค.
๐ ํ ์คํธ ์๋ํ
[ PlayerController.cs ]
using UnityEngine;
public class PlayerController : MonoBehaviour
{
Rigidbody rigid;
Animator anim;
public float speed = 3f;
public Vector3 inputKey = Vector3.zero;
void Awake()
{
rigid = GetComponent<Rigidbody>();
anim = GetComponent<Animator>();
}
void FixedUpdate()
{
Vector3 moveDir = inputKey != Vector3.zero ? inputKey : GetInputDirection();
if (moveDir != Vector3.zero)
MoveCharacter(moveDir.normalized);
else
anim.Play("Idle");
}
Vector3 GetInputDirection()
{
Vector3 direction = Vector3.zero;
if (Input.GetKey(KeyCode.W)) // ์์ผ๋ก ์ด๋
direction += Vector3.forward;
if (Input.GetKey(KeyCode.S)) // ๋ค๋ก ์ด๋
direction += Vector3.back;
if (Input.GetKey(KeyCode.D)) // ์ค๋ฅธ์ชฝ์ผ๋ก ์ด๋
direction += Vector3.right;
if (Input.GetKey(KeyCode.A)) // ์ผ์ชฝ์ผ๋ก ์ด๋
direction += Vector3.left;
return direction;
}
void MoveCharacter(Vector3 dir)
{
// ์บ๋ฆญํฐ ํ์
Quaternion targetRotation = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 10f);
// ๋ฌผ๋ฆฌ ์ด๋
Vector3 newPosition = rigid.position + dir * speed * Time.deltaTime;
rigid.MovePosition(newPosition);
anim.Play("RunForward");
}
}
[ PlayerControllerTest.cs ]
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.SceneManagement;
using NUnit.Framework;
using System.Collections;
using Unity.PerformanceTesting;
public class PlayerControllerTest
{
PlayerController playerController;
Animator animator;
string sceneName = "GameScene";
Vector3 startPos;
Vector3 endPos;
bool isIdle;
bool isRunning;
[UnitySetUp]
public IEnumerator Setup(){
if (SceneManager.GetActiveScene().name != sceneName) {
Debug.LogWarning("โ ๏ธ [TEST WARNING] ํ
์คํธ ์คํ ์ ์ฌ์ ๊ฐ์ ๋ก ๋ก๋ํฉ๋๋ค!");
SceneManager.LoadScene(sceneName);
}
while (SceneManager.GetActiveScene().name != sceneName)
yield return new WaitForSeconds(0.1f);
playerController = GameObject.Find("Player").GetComponent<PlayerController>();
if (playerController == null)
Assert.Fail("โ [TEST ERROR] PlayerController๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค!");
animator = GameObject.Find("Player").GetComponent<Animator>();
if (animator == null)
Assert.Fail("โ [TEST ERROR] Animator๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค!");
startPos = playerController.transform.position;
}
[UnityTest]
public IEnumerator MoveCharacterTest() {
playerController.inputKey = Vector3.forward;
yield return new WaitForSeconds(2f);
endPos = playerController.transform.position;
Assert.Greater(endPos.z, startPos.z, "โ [TEST Fail] ํ๋ ์ด์ด ์ด๋ ์คํจ!");
playerController.inputKey = Vector3.zero;
}
[UnityTest]
public IEnumerator AnimationIdleTest() {
playerController.inputKey = Vector3.zero;
yield return new WaitForSeconds(2f);
isIdle = animator.GetCurrentAnimatorStateInfo(0).IsName("Idle");
Assert.IsTrue(isIdle, "โ [TEST Fail] ํ๋ ์ด์ด๊ฐ ๋ฉ์ถฐ์์ง๋ง, 'Idle' ์ ๋๋ฉ์ด์
์ด ์คํ๋์ง ์์!");
}
[UnityTest]
public IEnumerator AnimationRunTest() {
playerController.inputKey = Vector3.left;
yield return new WaitForSeconds(2f);
isRunning = animator.GetCurrentAnimatorStateInfo(0).IsName("RunForward");
Assert.IsTrue(isRunning, "โ [TEST Fail] ํ๋ ์ด์ด๊ฐ ์ด๋ ์ค์ด์ง๋ง, 'RunForward' ์ ๋๋ฉ์ด์
์ด ์คํ๋์ง ์์!");
playerController.inputKey = Vector3.zero;
}
[Test, Performance]
public void MoveCharacterPerformanceTest() {
Measure.Method(() => {
playerController.inputKey = Vector3.forward;
playerController.FixedUpdate();
})
.WarmupCount(5)
.MeasurementCount(50)
.IterationsPerMeasurement(10)
.Run();
}
[TearDown]
public void TearDown() {
playerController = null;
animator = null;
isIdle = false;
isRunning = false;
startPos = Vector3.zero;
endPos = Vector3.zero;
}
}
[ CameraController.cs ]
using UnityEngine;
public class CameraController : MonoBehaviour
{
[SerializeField]
GameObject target;
public float offsetX = 0f;
public float offsetY = 2f;
public float offsetZ = -3f;
float speed = 3f;
Vector3 targetPos;
void FixedUpdate() {
// ์นด๋ฉ๋ผ ์ด๋
targetPos = target.transform.position + new Vector3(offsetX, offsetY, offsetZ);
transform.position = Vector3.Lerp(transform.position, targetPos, Time.deltaTime * speed);
}
}
[ CameraControllerTest.cs ]
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.SceneManagement;
using NUnit.Framework;
using System.Collections;
public class CameraControllerTest
{
PlayerController playerController;
CameraController cameraController;
string sceneName = "GameScene";
Vector3 initialCamPos;
Vector3 expectedCamPos;
Vector3 actualCamPos;
[UnitySetUp]
public IEnumerator SetUp() {
if (SceneManager.GetActiveScene().name != sceneName) {
Debug.LogWarning("โ ๏ธ [TEST WARNING] ํ
์คํธ ์คํ ์ ์ฌ์ ๊ฐ์ ๋ก ๋ก๋ํฉ๋๋ค!");
SceneManager.LoadScene(sceneName);
}
while (SceneManager.GetActiveScene().name != sceneName)
yield return new WaitForSeconds(0.1f);
playerController = GameObject.Find("Player").GetComponent<PlayerController>();
if (playerController == null)
Assert.Fail("โ [TEST ERROR] PlayerController๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค!");
cameraController = GameObject.Find("Camera").GetComponent<CameraController>();
if (cameraController == null)
Assert.Fail("โ [TEST ERROR] CameraController๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค!");
}
[UnityTest]
public IEnumerator CameraFollowTest() {
initialCamPos = cameraController.transform.position;
playerController.inputKey = Vector3.forward;
yield return new WaitForSeconds(1f);
playerController.inputKey = Vector3.zero;
yield return new WaitForSeconds(1f);
expectedCamPos = playerController.transform.position + new Vector3(cameraController.offsetX, cameraController.offsetY, cameraController.offsetZ);
actualCamPos = cameraController.transform.position;
Assert.AreNotEqual(initialCamPos, actualCamPos, "โ [TEST Fail] ์นด๋ฉ๋ผ๊ฐ ์ด๋ํ์ง ์์์ต๋๋ค!");
Assert.AreEqual(expectedCamPos.x, actualCamPos.x, 0.1f, "โ [TEST Fail] ์นด๋ฉ๋ผ X ์์น๊ฐ ์์๊ณผ ๋ค๋ฆ!");
Assert.AreEqual(expectedCamPos.y, actualCamPos.y, 0.1f, "โ [TEST Fail] ์นด๋ฉ๋ผ Y ์์น๊ฐ ์์๊ณผ ๋ค๋ฆ!");
Assert.AreEqual(expectedCamPos.z, actualCamPos.z, 0.1f, "โ [TEST Fail] ์นด๋ฉ๋ผ Z ์์น๊ฐ ์์๊ณผ ๋ค๋ฆ!");
}
[TearDown]
public void TearDown() {
playerController = null;
cameraController = null;
initialCamPos = Vector3.zero;
expectedCamPos = Vector3.zero;
actualCamPos = Vector3.zero;
}
}
[ โ ๏ธ ์ฃผ์์ฌํญ ]
ํ ์คํธ ์คํ ์ ์ ํ์ฌ Scene์ Unity ๋น๋ ์ค์ ์ ์ถ๊ฐํ์ง ์์ผ๋ฉด SceneManager.LoadScene()์ด ์คํ๋์ง ์์์ ์ฌ์ ๋ก๋ํ ์ ์๋ค.
1๏ธโฃ [ File ] > [ Build Profiles ]
2๏ธโฃ [ Scene List ] > [ Add Open Scenes ] ๋ฅผ ํตํด ํ์ฌ Scene์ Unity ๋น๋ ์ค์ ์ ์ถ๊ฐ
[ โ Why in Play Mode? ]
์ PlayerControllerTest.cs์ CameraControllerTest.cs๋ ๋ ๋ค Play Mode์์ ์คํํด์ผ ํ๋ค. ์ด์ ๋ ๋ฌด์์ผ๊น?
1๏ธโฃ Unity์ ์ ๋ ฅ ์์คํ ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ
~> PlayerController.cs๋ Input.GetKey()๋ฅผ ์ฌ์ฉํ์ฌ ํค ์ ๋ ฅ์ ๊ฐ์ง
~> ํ์ง๋ง Edit Mode ํ ์คํธ์์๋ Unity์ ์ ๋ ฅ ์์คํ ์ด ์๋ํ์ง ์์
2๏ธโฃ MonoBehaviour์ ๋์์ ํ ์คํธํ๊ธฐ ๋๋ฌธ
~> FixedUpdate(), Animator ๋ฑ Unity ์์ง์ ์ค์ ๋์์ ํฌํจํ๋ ํ ์คํธ๋ Play Mode์์๋ง ์คํ ๊ฐ๋ฅ
~> Edit Mode์์๋ MonoBehaviour๊ฐ ์คํ๋์ง ์์
3๏ธโฃ Scene์ ์กด์ฌํ๋ PlayerController, CameraController๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ
~> Edit Mode ํ ์คํธ์์๋ Scene์ ๋ก๋ํ์ง ์๊ณ ํ ์คํธ
~> Find()๋ฅผ ์ฌ์ฉํด์ Scene์ ์กด์ฌํ๋ Object๋ฅผ ์ฐพ์์ผ ํจ
๐ ํ ์คํธ ์๋ํ ์คํ (in Editor)

๊ธฐ๋ฅ | ์ค๋ช |
์คํ ๊ฒฝ๋ก | [ Window ] > [ General ] > [ Test Runner ] |
ํ ์คํธ ๋ชจ๋ | Edit Mode vs Play Mode |
๊ฒฐ๊ณผ ์ ์ฅ ์์น | Console ์ฐฝ ๋ก๊ทธ ํ์ธ |
Export Results | ํ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ํ์ผ๋ก ๋ด๋ณด๋ด๊ธฐ |
Clear Results | ๊ธฐ์กด ํ ์คํธ ๊ฒฐ๊ณผ ์ด๊ธฐํ |
Return Failed | ์คํจํ ํ ์คํธ๋ง ๋ค์ ์คํ |
Run Selected | ์ ํํ ํน์ ํ ์คํธ๋ง ์คํ |
Run All | ๋ชจ๋ ํ ์คํธ ์คํ |
๐ ํ ์คํธ ์๋ํ ๊ฒฐ๊ณผ (in Editor)

โ ์ ์ฅ ๊ฒฝ๋ก
๐ C:\Users\User\AppData\LocalLow\DefaultCompany\MyFirst3D
โ ํ์ผ๋ณ ํฌํจ ์ ๋ณด
ํ์ผ๋ช | ํฌํจ๋ ์ ๋ณด | ์ค๋ช |
TestResults.xml (by Unity Test Framework) |
- ์ ์ฒด ํ
์คํธ ๊ฐ์ - ์ฑ๊ณต/์คํจํ ํ ์คํธ ๊ฐ์ - ํ ์คํธ ์คํ ์๊ฐ - ์คํ ํ๊ฒฝ (PlayMode/EditMode) - ์ฑ๋ฅ ํ ์คํธ ๊ฒฐ๊ณผ ํฌํจ (์ต์/์ต๋ ์คํ ์๊ฐ ๋ฑ) ... |
๋ชจ๋ ํ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋กํ๋ฉฐ, ์ฑ๋ฅ ํ ์คํธ ๊ฒฐ๊ณผ๋ ํฌํจ๋จ |
PerformanceTestResults.json (by Unity Performance Testing Extension) |
- ํ๋์จ์ด ์ ๋ณด (CPU, GPU, RAM ๋ฑ) - ์คํ ํ๊ฒฝ (ํด์๋, ์ฃผ์ฌ์จ, ๋ ๋๋ง ๋ชจ๋ ๋ฑ) - ์ฑ๋ฅ ํ ์คํธ ๊ฒฐ๊ณผ๋ง ๋ณ๋๋ก ์ ์ฅ ... |
์ฑ๋ฅ ํ ์คํธ ๊ฒฐ๊ณผ๋ฅผ ์์ธํ ํ์ธํ ๋ ์ฌ์ฉ |
๐ ํ ์คํธ ์๋ํ ์คํ ๋ฐ ๊ฒฐ๊ณผ (in cmd)
Unity Test Runner๋ Unity Editor์์ ์คํํ ์๋ ์์ง๋ง, cmd์์๋ ์คํํ ์ ์๋ค. ์ฆ, GUI ์์ด cmd์์ ์๋ํ ํ ์คํธ๋ฅผ ์คํํ ์ ์์ผ๋ฉฐ, ์ด๋ฅผ ํ์ฉํ์ฌ CI/CD ํ๊ฒฝ์์๋ ํ ์คํธ๋ฅผ ์๋ํํ ์ ์๋ค.
[ ๋ช ๋ น์ด ์์ ]
"C:\Program Files\Unity\Hub\Editor\6000.0.38f1\Editor\Unity.exe" -batchmode -runTests -projectPath "C:/Users/User/UnityProject/MyFirst3D" -testPlatform PlayMode -logFile"C:/Users/User/UnityProject/MyFirst3D/results.log"
โ "C:\Program Files\Unity\Hub\Editor\6000.0.38f1\Editor\Unity.exe"
ใด Unity ์คํ ํ์ผ์ ๊ฒฝ๋ก๋ก ์ด๋ํ์ฌ ์คํ
โ -batchmode
ใด Unity๋ฅผ GUI ์์ด ํฐ๋ฏธ๋/์ปค๋งจ๋ ๋ผ์ธ์์ ์คํ (์๋ํ ์คํ์ ์ํด ํ์)
โ -runTests
ใด Unity์ TestRunner๋ฅผ ์คํํ์ฌ ํ๋ก์ ํธ์ ํ ์คํธ ์ํ
โ -projectPath "C:/Users/User/UnityProject/MyFirst3D"
ใด ํด๋น ๊ฒฝ๋ก์ Unity ํ๋ก์ ํธ๋ฅผ ์ด์ด์ ์คํ
โ -testPlatform PlayMode
ใด ํ ์คํธ๋ฅผ ์คํํ ํ๊ฒฝ์ ์ง์ (PlayMode: ์ค์ ๊ฒ์ ํ๊ฒฝ์์ ํ ์คํธ ์คํ)
โ -logFile "C:/Users/User/UnityProject/MyFirst3D/results.log"
ใด ํ ์คํธ ์คํ ๊ฒฐ๊ณผ๋ฅผ results.log ํ์ผ์ ์ ์ฅ
ใด ํ์ผ ๊ฒฝ๋ก๋ ์ ๋ ๊ฒฝ๋ก๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ (๊ฒฝ๋ก ๋ฌธ์ ๋ฐฉ์ง)

๐ Jira ์ด์ ๋ฑ๋ก ์๋ํ
Jira ์ด์ ๋ฑ๋ก ์๋ํ๋ฅผ ํ ์คํธํ๊ธฐ ์ํด, CameraControllerTest.cs ํ์ผ์ ์์ ํ์ฌ CameraFollowTest()๊ฐ ์คํจํ๋๋ก ๋ณ๊ฒฝํ์๋ค.
[ ์์ ์ ]
[UnityTest]
public IEnumerator CameraFollowTest() {
# ...
actualCamPos = cameraController.transform.position;
# ...
}
[ ์์ ํ ]
[UnityTest]
public IEnumerator CameraFollowTest() {
# ...
actualCamPos = cameraController.transform.position + new Vector3(1.0f, 1.0f, 1.0f);
# ...
}
[ ๊ตฌํ ์ฝ๋ ]
import os
import json
import requests
import xml.etree.ElementTree as ET
JIRA_PROJECT_KEY = "ํ๋ก์ ํธ ํค"
JIRA_URL = "jira ํ๋ก์ ํธ ๋๋ฉ์ธ"
JIRA_API_TOKEN = "๋ฐ๊ธ๋ฐ์ Jira API ํ ํฐ"
JIRA_EMAIL = "๋ณธ์ธ์ ์ด๋ฉ์ผ"
AUTH = (JIRA_EMAIL, JIRA_API_TOKEN)
HEADERS = {"Content-Type": "application/json"}
# Unity Test ๊ฒฐ๊ณผ XML ํ์ผ ๊ฒฝ๋ก
XML_FILE_PATH = "C:\\Users\\User\\AppData\\LocalLow\\DefaultCompany\\MyFirst3D\\TestResults.xml"
def getFailedTests(xmlPath): # ์คํจํ ํ
์คํธ ๋ชฉ๋ก์ ์ถ์ถํ๋ ํจ์
tree = ET.parse(xmlPath)
root = tree.getroot()
failedTests = []
for testCase in root.findall(".//test-case[@result='Failed']"):
testName = testCase.get("name")
messageNode = testCase.find("failure/message")
errorMessage = messageNode.text if messageNode is not None else "No error message provided"
failedTests.append({"name": testName , "message": errorMessage})
return failedTests
def createJiraIssue(summary, description): # Jira์ ๋ฒ๊ทธ ์ด์๋ฅผ ์์ฑํ๋ ํจ์
url = f"{JIRA_URL}/rest/api/2/issue/"
data = {
"fields": {
"project": {"key": JIRA_PROJECT_KEY},
"summary": summary,
"description": description,
"issuetype": {"name": "๋ฒ๊ทธ"}
}
}
response = requests.post(url, auth=AUTH, headers=HEADERS, data=json.dumps(data))
if response.status_code == 201:
print(f"โ
Jira ์ด์ ์์ฑ ์๋ฃ: {summary}")
else:
print(f"โ Jira ์ด์ ์์ฑ ์คํจ: {response.text}")
def processTestResults(): # ์คํจํ ํ
์คํธ๋ฅผ ๋ถ์ํ๊ณ Jira์ ์ด์๋ฅผ ์์ฑํ๋ ํจ์
failed_tests = getFailedTests(XML_FILE_PATH)
if not failed_tests:
print("๐ ๋ชจ๋ ํ
์คํธ๊ฐ ์ฑ๊ณตํ์ต๋๋ค. Jira ์ด์๋ฅผ ๋ฑ๋กํ ํ์๊ฐ ์์ต๋๋ค.")
return
print(f"๐จ {len(failed_tests)}๊ฐ์ ํ
์คํธ๊ฐ ์คํจํ์ต๋๋ค. Jira ์ด์๋ฅผ ์์ฑํฉ๋๋ค...")
for test in failed_tests:
summary = f"[Unity Test] {test['name']} ์คํจ"
description = f"ํ
์คํธ '{test['name']}' ์คํจ: {test['message']}"
createJiraIssue(summary, description)
# ์คํ
os.system('cls')
processTestResults()
[ ์คํ ๊ฒฐ๊ณผ ]


Jira ์ด์ ๋ฑ๋ก ์๋ํ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ ๊ฒฐ๊ณผ, ์์ ๊ฐ์ด Jira์ ์ด์๊ฐ ์๋์ผ๋ก ๋ฑ๋ก๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
'๐ Quality Assurance Study > ๐ QA ๊ณต๋ถ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Jira REST API ํ ํฐ ๋ฐ๊ธ ๋ฐ ์ด์ ์กฐํ (0) | 2025.03.02 |
---|---|
XPath์ ๋ํ์ฌ (0) | 2025.02.27 |
Jira ๋ฐ Confluence ์ฌ์ฉ ๋ฐฉ๋ฒ (2) | 2025.02.20 |
BTS (Bug Tracking System)์ ๋ํ์ฌ (3) | 2025.01.13 |