问题描述
我已经在网上阅读了许多关于如何设置单元测试的文章,最重要的是,它看起来非常简单:使用 Unity 中的测试运行创建一个测试目录。根据{{3}},如果您遇到命名空间问题,那么您可以直接在脚本中创建一个程序集定义文件,并在您的 test.asmdef
文件中引用它,然后您就可以成功开始运行测试了。>
我的问题是我继承了一个包含 34 个 Scripts
目录的项目,当我向其中添加一个程序集定义文件时,它会与所有其他命名空间/对象产生命名空间问题。合乎逻辑的结论是我在每个文件中创建了一个 .asmdef
并在需要的地方创建引用。不幸的是,该程序的设计方式会在程序集定义文件之间创建循环依赖关系。这种循环依赖在程序的一般使用中不是问题。在不重构代码库的情况下,有没有办法让这段代码可测试?
解决方法
简单的解决方案是将 asmdef 添加到 34 个脚本文件夹的顶部文件夹。
如果它们都在 Assets 文件夹中,那么您可以创建该 Script 文件夹并将它们全部移到那里。这不会破坏您的项目,因为 Unity 会更新所有连接。
您可能需要采取的长期解决方案是在汇编中创建当前代码将实现的抽象/接口。
假设你在 player.asmdef 中有脚本 Player 并且你想测试它。但是它对 Inventory 有依赖性,而 Inventory 不在任何 asmdef 中。您可以移动 Inventory,但它也有它的一组依赖项等等。
不是移动库存,而是在 manager.asmdef 中创建一个基本库存作为抽象和接口,并将其添加到 player.asmdef。假设 Player.cs 使用
List<Item> Inventory.GetInventory();
void Inventory.SetItem(Item item);
您的 IInventory.cs 可能看起来像这样
public abstract class InventoryBase : MonoBehaviour,IInventory
{
// if those methods were self contained,meaning they don't use any outside code
// the implementation could be moved here
public abstract List<Item> GetInventory();
public abstract void SetItem(Item item);
}
public interface IInventory
{
List<Item> GetInventory();
void SetItem(Item item);
}
public class Item
{
public string id;
public int amount;
public string type;
}
然后是库存类
public class Inventory : InventoryBase
{
// Implementation is already there since it was used
// but requires the override on the methods
}
可能感觉像是添加了额外的无用层,但这增加了第二个非常重要的优势,您可以在播放器测试中模拟 IInventory 对象:
[Test]
public void TestPlayer()
{
// Using Moq framework but NSubstitute does same with different syntax
Mock<IInventory> mockInventory = new Mock<IInventory>();
Mock<IPlayer> mockPlayer= new Mock<IPlayer>();
PlayerLogic player = new PlayerLogic(mockPlayer.Object,mockInventory.Object);
mock.Setup(m=> m.GetInventory).Returns(new List<Item>());
}
这假设 Player 类在 MonoBehaviour 和逻辑之间是解耦的:
public class Player : MonoBehaviour,IPlayer
{
[SerializedField] private InventoryBase m_inventory;
PlayerLogic m_logic;
void Awake()
{
m_logic = new PlayerLogic(this,m_inventory);
}
}
public interface IPlayer{}
public class PlayerLogic
{
IPlayer m_player;
IInventory m_inventory
public PlayerLogic(IPlayer player,IInventory inventory)
{
m_player = player;
m_inventory = inventory;
}
// Do what you need with dependencies
// Test will use the mock objects as if they were real
}
注意 Player 使用 InventoryBase,因为它看不到 Inventory 不在程序集中。但是当您放入 Inventory 对象时,即使 Player 类型不知道 Inventory 类型,编译器也会使用那里的代码。
如果您要使用 Inventory 中的其他方法到 Player 中,则需要将抽象添加到基类和接口中的声明以进行测试。
PlayerLogic 使用接口而不是基类型来使测试成为可能。