概要
UniTaskを使ってuGUI制御をします。
モデル部分は単体テストを行います。
開発環境
Unity:2021.3.9
IDE:Rider
UniTask:2.3.3
図
ユースケース
1.メインメニューのボタンを押すと、黒ウィンドウが表示される。
2. 黒ウィンドウのボタンを押すと黒ウィンドウは非表示になり、処理が完了する。(今回は何の処理も入れていない。)
3. 黒ウィンドウ表示時に右クリックすると、 黒ウィンドウは非表示になり処理が初期化される。
クラス図
実現したいこと
UI部分とモデル部分を分離させたい。
→入力した情報をboolean型に変換することで分離させる。
モデル部分の単体テストを書きたい。(実装する際にモデルの部分とUIの部分を同時に実装するのは面倒なので、一つ一つ片付けたい。)
→boolean型で擬似的に入力情報を再現できるのでテストコードが書ける。
コード部分
プロダクトコード
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class MainMenuPresenterRefactor : MonoBehaviour
{
[Header("Black")]
[SerializeField] private Button _blackWindowButton;
[SerializeField] private GameObject _blackWindows;
[SerializeField] private Button _blackWindowOkButton;
[SerializeField] private BlackWindowTask _blackWindowTask;
// it is used by input data.
private CancellationTokenSource inputCts = new CancellationTokenSource();
private void Start()
{
_blackWindowTask = new BlackWindowTask();
BlackWindow();
WaitForBlackInputToBoolean();
_blackWindows.gameObject.SetActive(false);
}
/// <summary>
/// This method`role is converting from Input( such as MousePointerDown and KeyButtonDown ) to boolean type,Because of splitting GUI and MainLogic.
/// the spiriting help easy to make UnitTest in MainLogic.
/// </summary>
private async void WaitForBlackInputToBoolean()
{
while (true)
{
var result = await UniTask.WhenAny(
_blackWindowButton.OnClickAsync(inputCts.Token),
_blackWindowOkButton.OnClickAsync(inputCts.Token),
UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Mouse1), PlayerLoopTiming.Update,
inputCts.Token)
);
if (result == 0)
{
_blackWindowTask.IsMenuBlackButton = true;
Debug.Log("MenuBlackButtonOnClick make ");
}
if (result == 1)
{
_blackWindowTask.IsBlackOkButton = true;
Debug.Log("result Click" + result);
}
if (result == 2 && _blackWindowTask.IsMenuBlackButton)
{
_blackWindowTask.IsCancel = true;
Debug.Log("result Click" + result);
}
}
}
/// <summary>
/// This method is charge of controlling player`s operation.
///
/// <see cref="BlackWindowTaskTest"/>
/// </summary>
public async void BlackWindow()
{
while (true)
{
_blackWindowTask.Cts = new CancellationTokenSource();
await UniTask.WhenAny(_blackWindowTask.WaitForMenuButton());
_blackWindows.gameObject.SetActive(true);
_blackWindowButton.enabled = false;
await UniTask.WhenAll(_blackWindowTask.WaitForOkButton(), _blackWindowTask.WaitForCancel(),
_blackWindowTask.FreeCancellationToken());
_blackWindows.gameObject.SetActive(false);
_blackWindowButton.enabled = true;
}
}
}
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
/// <summary>
///
/// </summary>
public class BlackWindowTask : MonoBehaviour
{
private bool _isBlackOkButton = false;
private bool _isMenuBlackButton = false;
private bool _isCancel;
private CancellationTokenSource cts = new CancellationTokenSource() ;
public CancellationTokenSource Cts
{
get => cts;
set => cts = value;
}
public bool IsBlackOkButton
{
get => _isBlackOkButton;
set => _isBlackOkButton = value;
}
public bool IsMenuBlackButton
{
get => _isMenuBlackButton;
set => _isMenuBlackButton = value;
}
public bool IsCancel
{
get => _isCancel;
set => _isCancel = value;
}
/// <summary>
/// This method is Clearning Method.
/// this method does iniitallizing boolean and cts.
/// </summary>
/// <returns></returns>
public async UniTask<bool> FreeCancellationToken()
{
try
{
await UniTask.WaitUntil( () => cts.IsCancellationRequested, PlayerLoopTiming.Update, cts.Token);
return true;
}
catch (OperationCanceledException e)
{
cts = null;
Debug.Log("CancelationToken is null");
_isCancel = false;
_isBlackOkButton = false;
_isMenuBlackButton = false;
return false;
}
}
public async UniTask<bool> WaitForMenuButton()
{
try
{
await UniTask.WaitUntil(() => _isMenuBlackButton, PlayerLoopTiming.Update, cts.Token);
return true;
}
catch (OperationCanceledException e)
{
Debug.Log("BlackMenuButton is Canceled");
return false;
}
}
public async UniTask<bool> WaitForOkButton()
{
try
{
await UniTask.WaitUntil(() => _isBlackOkButton, PlayerLoopTiming.Update, cts.Token);
cts.Cancel();
return true;
}
catch (OperationCanceledException e)
{
Debug.Log("BlackSubMenuOkButton is Canceled");
return false;
}
}
public async UniTask<bool> WaitForCancel()
{
try
{
await UniTask.WaitUntil(() =>_isCancel,PlayerLoopTiming.Update,cts.Token);
Debug.Log("Right Click is pressed.");
cts.Cancel();
return true;
}
catch (OperationCanceledException e)
{
Debug.Log("BlackSubMenu画面においてCancelボタン(右クリック)がキャンセルされました。");
return false;
}
}
}
テストコード
using System.Collections;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using UnityEngine.TestTools;
using Is = UnityEngine.TestTools.Constraints.Is;
public class BlackWindowTaskTest
{
// positive pattern
[UnityTest]
public IEnumerator givenSucessInput_whenTask_thenTaskSucess() =>
UniTask.ToCoroutine(async () =>
{
// given prepare
BlackWindowTask _blackWindowTask = new BlackWindowTask();
// when -do
// Click button "black" in Menu
_blackWindowTask.IsMenuBlackButton = true;
var actualMainButton = await _blackWindowTask.WaitForMenuButton();
// Click button"OK" on BlackWindow in actual game.
_blackWindowTask.IsBlackOkButton = true;
var (actualIsBlackOkButton, actualCancel, actualFreeCanceletion) =
await UniTask.WhenAll(_blackWindowTask.WaitForOkButton(), _blackWindowTask.WaitForCancel(), _blackWindowTask.FreeCancellationToken());
// then verify boolean
await UniTask.Delay(500);
// Check Each Task result
Assert.That(actualMainButton, Is.EqualTo(true));
Assert.That(actualIsBlackOkButton, Is.EqualTo(true));
Assert.That(actualCancel,Is.EqualTo(false));
Assert.That(actualFreeCanceletion,Is.EqualTo(false));
// Check initializing bool and Reset Cts
Assert.That(_blackWindowTask.IsMenuBlackButton, Is.EqualTo(false));
Assert.That(_blackWindowTask.IsCancel, Is.EqualTo(false));
Assert.That(_blackWindowTask.IsBlackOkButton, Is.EqualTo(false));
Assert.That(_blackWindowTask.Cts, Is.EqualTo(null));
});
// negative pattern
[UnityTest]
public IEnumerator givenCancelInput_whenTask_thenTaskCancel() =>
UniTask.ToCoroutine(async () =>
{
// given prepare
BlackWindowTask _blackWindowTask = new BlackWindowTask();
// when -do
// Click button in Menu
_blackWindowTask.IsMenuBlackButton = true;
var actualMainButton = await _blackWindowTask.WaitForMenuButton();
// Click button in actual game.
_blackWindowTask.IsCancel = true;
// _blackWindowTask.IsCancel=_isCancel;
var (actualIsBlackOkButton, actualCancel, actualFreeCanceletion) =
await UniTask.WhenAll(_blackWindowTask.WaitForOkButton(), _blackWindowTask.WaitForCancel(), _blackWindowTask.FreeCancellationToken());
// then verify boolean
await UniTask.Delay(500);
// Check Each Task result
Assert.That(actualMainButton, Is.EqualTo(true));
Assert.That(actualIsBlackOkButton, Is.EqualTo(false));
Assert.That(actualCancel,Is.EqualTo(true));
Assert.That(actualFreeCanceletion,Is.EqualTo(false));
// Check initializing bool and Reset Cts
Assert.That(_blackWindowTask.IsMenuBlackButton, Is.EqualTo(false));
Assert.That(_blackWindowTask.IsCancel, Is.EqualTo(false));
Assert.That(_blackWindowTask.IsBlackOkButton, Is.EqualTo(false));
Assert.That(_blackWindowTask.Cts, Is.EqualTo(null));
});
}
テスト時の様子
感想
苦労した点
非同期の単体テストの実践例が見つけられなくて、試行錯誤の量が多かったです。
UdemyでC# Nunitの講座を確認しても非同期の項目は薄くてあまり参考になりませんでした。
良かった点
GUIとモデル部分を分離できたこと。
モデル部分のテストコードがあるので、GUIの実装をする際モデル部分を確認しなくて、実装がし易かったです。。
参考資料
クリーンアーキテクチャー
25:00ぐらい
「インプットの情報を変換するクラス」を参考にしました。
https://www.youtube.com/watch?v=BvzjpAe3d4g
セールのときに視聴しときたい(参考予定) C# のTaskですが、UniTaskにも応用できると私は思います。
https://www.udemy.com/course/cs-asyncawait-1/