【问题标题】:Fake singleton with FakeItEasy使用 FakeItEasy 伪造单例
【发布时间】:2017-03-12 14:09:40
【问题描述】:

我在测试单例时遇到了一些问题。当我运行此代码时,我在 TestGetLogicalDevices() 中遇到错误。 CallTo() 失败,因为服务不是假对象。当我尝试创建一个假对象(注释代码)时,它给出了一个错误,因为 RestService 是一个带有私有构造函数的单例。如何创建这个单例的假对象?

    private RestService service;

    [TestInitialize]
    public void Init()
    {
        //service = A.Fake<RestService>();
        service = RestService.Instance;
        service.CreateClient("test", "test");
    }

    [TestMethod]
    public async Task TestGetLogicalDevices()
    {
        var logicalDevices = (List<LogicalDevice>)A.CollectionOfFake<LogicalDevice>(10);
        A.CallTo(() => service.GetLogicalDevices()).Returns(Task.FromResult(logicalDevices));
        List<LogicalDevice> collectedData = await service.GetLogicalDevices();
        Assert.AreEqual(2, collectedData.Count);
    }

    public async Task<List<LogicalDevice>> GetLogicalDevices()
    {
        var response = await client.GetAsync(apiBaseUrl + "/logical-devices");
        if (response.IsSuccessStatusCode)
        {
            var json = await response.Content.ReadAsStringAsync();
            var logicalDevices = JsonConvert.DeserializeObject<List<LogicalDevice>>(json);
            var sortedList = logicalDevices.OrderBy(logicalDevice => logicalDevice.Name).ToList();
            return sortedList;
        }
        else
        {
            return null;
        }
    } 

更新 我添加了我想要测试的方法的代码。也许有人对更好的测试有建议?

【问题讨论】:

  • 请发布 RestService 的代码,至少是相关位。
  • 您不能使用模拟框架启动一个具体的类来表现您希望它的行为。这就是假货的用途。它们是依赖项的虚假版本,可用于测试依赖它们的类。目前尚不清楚您要通过此测试断言什么。
  • @tomredfern 我添加了我要测试的方法的代码。也许你有更好的解决方案来测试这个?
  • client.GetAsync(...)中客户端的类型是什么?
  • @tomredfern 这是一个带有身份验证的 HttpClient。这是代码:byte[] authBytes = Encoding.UTF8.GetBytes(Username + ":" + Password); client = new HttpClient(); var authHeaderValue = Convert.ToBase64String(authBytes); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authHeaderValue);

标签: c# unit-testing mocking singleton fakeiteasy


【解决方案1】:

注意:我不确定我是否理解您要执行的操作。你到底想测试什么?在您的测试中,您将 service.GetLogicalDevices() 配置为返回某些内容,然后调用 service.GetLogicalDevices() 并断言它返回的内容(除非 FakeItEasy 被破坏,否则应该是您将其配置为返回的内容)。因此,您实际上并不是在测试服务……您是在测试模拟框架!像 FakeItEasy 这样的模拟框架对于模拟被测系统 (SUT) 的依赖关系很有用,而不是 SUT 本身。在您的情况下,如果 SUT 是 RestService,您需要模拟 RestService 的依赖关系,而不是 RestService 本身。例如,您可以使用您控制的HttpMessageHandler 注入HttpClient(有关详细信息,请参阅here)。


现在,回答你的实际问题(假设你真的想伪造RestService):

当我运行此代码时,我在 TestGetLogicalDevices() 中遇到错误。 CallTo() 失败,因为服务不是假对象。

A.CallTo 仅适用于假货; FakeItEasy 无法控制不是它创建的对象的行为。

当我尝试创建一个假对象(注释代码)时,它会出错,因为 RestService 是一个带有私有构造函数的单例

RestService是一个类,FakeItEasy可以为一个类创建一个fake,但是它是通过继承类来实现的,所以它需要一个可访问的构造函数。另外,请记住,只能配置 virtual 方法。 GetLogicalDevices 不是虚拟的,所以假的不能覆盖它的行为。

你有两个主要的方法来伪装RestService

  • 将构造函数设为protected 而不是private,并将方法设为虚拟以便可以覆盖它们
  • 创建一个代表RestService 类的“公共合同”的IRestService 接口,并伪造该接口而不是该类。

【讨论】:

  • 实际上我正在尝试测试 GetLogicalDevices() 方法,但我不知道从哪里开始。我认为假货可以让我在不进行 api 调用的情况下获得响应数据。也许你能给我举个例子? @ThomasLevesque
  • @JorisMeylaers 如果你在测试GetLogicalDevices(),你不能嘲笑它,它必须是真实的。您需要模拟的是依赖项。在这种情况下,是HttpClient,不能直接mock,但是可以mock HttpClient使用的HttpMessageHandler。我指出的文章显示了一个例子。我将尝试发布一个更具体的示例。
  • @JorisMeylaers,这是您案例的完整示例:gist.github.com/thomaslevesque/622f8a0118ee1f2d46d723db2a976985。请注意,您不能将 RestService 保留为真正的单例,因为构造函数需要公开,但无论如何您可能不应该这样做;当涉及到单元测试时,单例模式是一种糟糕的模式。您应该改用依赖注入。