รู้จัก Test double แต่ละประเภท และการใช้งาน
11 Sep 2023 14:02
Written by: Yosapol Jitrak
ก่อนหน้านี้ผมกล่าวถึงการทำ TDD อย่างถูกต้อง ซึ่งมีการกล่าวถึง Test double อยู่เพียงเล็กน้อย ในบทความนี้ผมจะมากล่าวถึง Test double อย่างละเอียดครับ โดยยกตัวอย่างเป็น C# ด้วย NUnit แต่หลักการสามารถใช้กับภาษาอื่นได้เช่นกันครับ
ปกติการทำ Software คงเลี่ยงยากที่จะไม่ยุ่งกับ Dependencies หรือ Third party เช่น API, Database, Time, Classes, Sharded Properties เป็นต้น
Credit image: 10 Common Software Architectural Patterns in a nutshell
ซึ่งเราจะแทนที่ Dependencies ด้วย Test double ตามรูปนี้ครับ
SUT ย่อมาจาก System Under Test นะครับ
ยกตัวอย่าง ให้เห็นภาพชัดขึ้น Credit image: Test Doubles — Fakes, Mocks and Stubs.
แทนที่เราจะต่อ AcountDAO ซึ่งต่อกับ Database จริง เราก็มาต่อกับ FakeAccountDAO แทน คิดว่าพอจะเห็นภาพกันมากขึ้นนะครับ เดี๋ยวเราจะมาดู Test double แต่ละประเภทกันครับ
Credit image: Mocking in python
ตัวอย่างทั้งหมดนี้เอามาจาก Unit testing at the speed of light with Unity Test Tools ซึ่งตัว Blog เก่ามากแล้ว รูปภาพที่ได้ทำการยกตัวอย่างไว้แสดงได้ไม่ครบถ้วน
ใช้เมื่อต้องการใช้ Object type นั้น อย่าง Interface หรือ Class โดยไม่ได้สนใจ Implement ข้างใน แต่ต้องส่ง Object ไปให้ Method อื่น ๆ ใช้งาน
public interface ISpaceShip
{
int AvailableWeaponSlots { get; }
void Equip(IWeapon weapon);
}
public class DummyWeapon : IWeapon
{
public Shot[] Shoot()
{
return null;
}
}
[Test]
public void NoWeaponSlotsAvailable_AfterWeaponIsEquiped()
{
// Arrange
SpaceShip spaceShip = SpaceShipWithSingleWeaponSlot();
IWeapon weapon = new DummyWeapon();
// Act
spaceShip.Equip(weapon);
// Assert
var noWeaponSlotsAvailable = spaceShip.AvailableWeaponSlots == 0;
Assert.That(noWeaponSlotsAvailable);
}
จะเห็นว่าตัว DummyWeapon ไม่ได้ Implement อะไรเป็นพิเศษเลย เพียงแค่ให้สามารถสร้าง Object จาก Interface IWeapon ได้ แล้วส่งไปให้ Method Equip ของ SpaceShip ใช้งานได้
แต่ดูวิธีการสร้าง Class DummyWeapon มือเป็นวิธีการที่ถึกไปหน่อย ถ้า Interface IWeapon ไม่ได้มี Method แค่นี้ จึงแนะนำให้ใช้ Mocking libraries ที่มีการสร้าง Dummy Object ให้เราเลยครับ ในที่นี้ผมจะใช้ NSubstitute ภาษาอื่นที่ไม่ใช่ C# ก็มี Mocking libraries ให้ใช้งานเช่นกันครับ
[Test]
public void NoWeaponSlotsAvailable_AfterWeaponIsEquiped()
{
// Arrange
SpaceShip spaceShip = SpaceShipWithSingleWeaponSlot();
IWeapon weapon = NSubstitute.Substitute.For<IWeapon>();
// Act
spaceShip.Equip(weapon);
// Assert
var noWeaponSlotsAvailable = spaceShip.AvailableWeaponSlots == 0;
Assert.That(noWeaponSlotsAvailable);
}
พอใช้ Mocking libraries แล้ว ก็จะเห็นว่าไม่ต้องสร้าง DummyWeapon class เองแล้ว
ใช้เมื่อต้องการบังคับ Return value จาก Dependency ครับ
class FunctionalWeaponStub: IWeapon
{
public Shot[] Shoot()
{
return new[] { new Shot(0, 0, 0) };
}
}
[Test]
public void SpaceShipShootsAtLeastOneShot_WhenFunctionalWeaponIsEquiped()
{
// Arrange
var ship = SpaceShipWithSingleWeaponSlot();
ship.Equip(new FunctionalWeaponStub());
// Act
var round = ship.Shoot();
// Assert
var roundContainsOneShot = round.Length == 1;
Assert.That(roundContainsOneShot);
}
แน่นอนว่าการเขียน Class FunctionalWeaponStub เป็นอะไรที่เปลืองแรงเช่นเคย ก็ใช้ NSubstitute แทนได้เช่นกันครับ
[Test]
public void SpaceShipShootsAtLeastOneShot_WhenFunctionalWeaponIsEquiped()
{
// Arrange
var ship = SpaceShipWithSingleWeaponSlot();
var functionalWeaponStub = Substitute.For<IWeapon>();
functionalWeaponStub.Shoot().Returns(new[] { new Shot(0, 0, 0) });
ship.Equip(functionalWeaponStub);
// Act
var round = ship.Shoot();
// Assert
var roundContainsOneShot = round.Length == 1;
Assert.That(roundContainsOneShot);
}
ยกตัวอย่าง Stub อีกตัวหนึ่งครับ
var randomNumberService = Substitute.For<IRandomNumberService>();
randomNumberService.Range(0, 0).ReturnsForAnyArgs(0, 2, 8);
จากตัวอย่าง Code นี้คือตัวอย่างการบังคับให้ Random คืนค่าตามที่ต้องการได้ โดยเรียกครั้งแรกจะคืนค่า 0 ออกมา และเมื่อเรียกครั้งถัดไปจะคืนค่า 2 ออกมา และครั้งที่สามจะคืนค่า 8 ออกมา
เอาไว้ติดตามหรือบันทึกการเรียกใช้งานของ Dependency ครับ
public class SpaceShipSpy : SpaceShipDummy
{
public int HitsCount { get; set; }
public override void AcceptIncomingShots(IEnumerable<Shot> shots)
{
HitsCount+=shots.Count();
}
}
[Test]
public void OpponentGetHitInAnEncounter_OnAttack()
{
// Arrange
var opponent = new SpaceShipSpy();
var player = SpaceShipWithTwoFunctionalWeaponStubs();
var randomNumberService = AlwaysMaxRandomNumber();
var encounter = new Encounter(player, opponent, randomNumberService);
// Act
encounter.Attack();
// Assert
Assert.That(opponent.HitsCount, Is.EqualTo(2));
}
ซึ่งเราสามารถแทนการสร้าง SpaceShipSpy ได้ด้วย Code ข้างล่างนี้ครับ
int hitCount = 0;
var opponent = Substitute.For<ISpaceShip>();
opponent.AcceptIncomingShots (Arg.Do<IEnumerable<Shot>>(x => hitCount+=x.Count()) );
เป็น Test Spy ที่มีการตรวจสอบการเรียกใช้งานของ Dependency ครับ
[Test]
public void EachWeaponShoots_WhenSpaceShipShootIsCalled()
{
// Arrange
var weapon1 = Substitute.For<IWeapon>();
var weapon2 = Substitute.For<IWeapon>();
var ship = new SpaceShip(2, 0);
ship.Equip(weapon1);
ship.Equip(weapon2);
// Act
ship.Shoot();
// Assert
weapon1.Received(1).Shoot();
weapon2.Received(1).Shoot();
}
จากตัวอย่าง Code ข้างบน จะเป็นการเช็คว่า weapon1 และ weapon2 ถูกเรียกใช้งาน Method Shoot 1 ครั้งหรือไม่
[Test]
public void EachWeaponGetsReloaded_AfterItIsShot()
{
// Arrange
var weapon1 = Substitute.For<IWeapon>();
var weapon2 = Substitute.For<IWeapon>();
var ship = new SpaceShip(2, 0);
ship.Equip(weapon1);
ship.Equip(weapon2);
// Act
ship.Shoot();
// Assert
Received.InOrder(() =>
{
weapon1.Shoot();
weapon2.Shoot();
weapon1.Reload();
weapon2.Reload();
}
);
}
จากตัวอย่าง Code ข้างบน จะเป็นการเช็คว่า Method ต่าง ๆ ถูกเรียกใช้งานตามลำดับหรือไม่
เป็น Test double ที่มี Logic อยู่ข้างใน ซึ่งจำลองการทำงานเหมือนระบบจริง และควรพยายามเลี่ยง Fake object ถ้าเป็นไปได้มากที่สุด ขนาดลุง Bob ยังบอกว่าทำ Software ใน 30 ปีที่ผ่านมา เคยใช้ Fake oject เพียงแค่ครั้งเดียวเอง [แปล] The Little Mocker ของ Uncle Bob
Code นี้เป็น Java นะครับ
public class AcceptingAuthorizerStub implements Authorizer {
public Boolean authorize(String username, String password) {
return username.equals("Bob");
}
}
ย้ำอีกครั้งนะครับตัวอย่างเกือบทั้งหมดนี้เอามาจาก Unit testing at the speed of light with Unity Test Tools ผมไม่ได้เขียนเองครับ
จบไปแล้วนะครับ กับ Test double บทความหน้า ผมจะมาต่อเรื่อง npm package ที่ช่วยให้เขียน Test double กับ Jest ได้ง่าย
ไปละครับ Bye Bye