【问题标题】:What can i test on these types of methods?我可以对这些类型的方法进行什么测试?
【发布时间】:2017-12-18 17:40:16
【问题描述】:

对不起这个糟糕的标题,我已经尽力了。 也许有更多想象力的人可以帮助我和这个标题。

我是单元测试的新手,我有点迷茫。无论如何,我正在阅读“单元测试的艺术”。

我正在为 WebApi 应用程序使用 Entity Framework 6。 在这个申请中,我使用这种模式:

Api 调用调用 Repository 的 Service,该 Repository 将数据返回给将数据返回给 API 的服务。

所以,我有很多这样的方法:

API:

public async Task<IHttpActionResult> GetUserById(Guid id)
{
    try
    {
        UserService userService = new UserService();
        return Ok(await userService.GetById(id));
    }
    catch(Exception ex)
    {
        return InternalServerError(ex);
    }
}

服务:

public class UserService
{
    private IUserRepository userRepo;
    public IUserRepository userRepo { get => userRepo; set => userRepo = value }

    public async Task<AspNetUser> GetById(Guid id)
    {
        if(id == Guid.Empty() || id == null)
            return null;

        return await Task.Run(() => userRepo.GetById(id));
    }
}

回购:

public interface IUserRepository
{
    Task<AspNetUser> GetById(Guid id);
}

public class UserRepository: IUserRepository
{

    public async Task<AspNetUser> GetById(Guid id)
    {
        return DbSet.Where(i => i.Id == id).FirstOrDefault();
    }
}

这里有什么问题?会出什么问题?我应该测试什么?

【问题讨论】:

  • 或许在这里开始考虑“在接口级别进行测试”?即GetById(Guid id) 在那里。
  • 删除你的实现以开始并思考我希望这个类封装什么行为。
  • 我用正确的例子更新了我的帖子。我把最后一个代码弄乱了。 “接口级别的测试”是什么意思? @MarkSchultheiss
  • @ImFlash 你想从给定的输入中得到什么输出
  • @ImFlash 是的,这是您应该编写的测试,我发现将它们分解为 GWT(Given When Thens)会有所帮助,假设存在具有 ID 的用户,当通过 ID 获取用户时,然后用户返回

标签: c# visual-studio entity-framework unit-testing entity-framework-6


【解决方案1】:

单元测试的目标是单独测试代码的每个模块(通常等同于方法)。这只有在松耦合的情况下才有可能。在您现有的代码中,您不使用 DI(依赖注入),而是在您的方法中实例化服务/存储库类。如果不是几乎不可能的话,这使得单独测试这段代码变得很困难。编写单元测试经常会暴露出这样的设计/代码缺陷。重构代码如下:


  1. 使用接口,你是在你的存储库上做的,但不是在你的服务上,但这实际上是使用接口的最佳位置。
  2. 使用依赖注入。这将使测试变得更加容易。
  3. 为什么要围绕 EF 使用存储库模式? EF 的 DbSet 是存储库模式的通用实现,而 DbContext 是工作单元的实现。我强烈建议不要尝试将这些重新封装在您自己半生不熟的抽象中,您最终只会让您自己更难使用自己的构造。
  4. 这不是使用异步/等待的正确方法。调用异步方法,如果不存在,则不要创建以开始的异步方法(这不适用于您要使用 TPL 的情况)。在您的情况下,您应该致电FirstOrDefaultAsync(或可能的SingleOrDefaultAsync);
  5. 使用后缀 Async 命名您的异步方法,这被认为是正确的约定。

UserController.cs

private readonly IUserService userService;
public UserController(IUserService userService){
  this.userService = userService;
}

public async Task<IHttpActionResult> GetUserById(Guid id)
{
    try
    {
        return Ok(await userService.GetByIdAsync(id));
    }
    catch(Exception ex)
    {
        return InternalServerError(ex);
    }
}

UserService.cs

public interface IUserService{
  Task<AspNetUser> GetByIdAsync(Guid id);
}

public class UserService : IUserService
{
    // you can leave MyDbContext unsealed or you can use an interface on this as well depending on your needs
    private readonly MyDbContext dbContext;
    public UserService(MyDbContext dbContext) {
      this.dbContext = dbContext;
    }

    public Task<AspNetUser> GetByIdAsync(Guid id)
    {
        if(id == Guid.Empty())
            return Task.FromResult(null as AspNetUser);
        return dbContext.AspNetUsers.SingleOrDefaultAsync(user => user.Id == id);
    }
}

现在您可以对代码进行单元测试了。您可以使用流行的 fake/mock/substitute 框架并执行以下操作:

  1. 通过在GetByIdAsync 上创建具有自定义行为的模拟IUserService 实例,为您的api 的GetUserById 创建一个测试
  2. 通过创建MyDbContext 的模拟来为UserService::GetByIdAsync 方法创建一个测试,您可以提供AspNetUser 实例的集合并测试调用该方法时返回哪个实例,甚至测试DbSet 的表达式调用。

您应该测试的内容取决于可能的输入和相应的预期结果。例如:当您将一个空的 GUID 传递给 GetByIdAsync 时会发生什么?当Task返回空结果时,API会做什么?等等

【讨论】:

  • 不错。我一定会做到的。在第 4 点中,我在使用(我认为)我的 Visual Studio 时遇到了 EF 和 Async/await 问题,因为当我使用 FirstOrDefaultAsync 或 ListAsync 时,任务仍处于“等待激活”状态。我还没有找到解决方案。第 3 点:我使用存储库,因为在那里我有重要的方法,例如“删除”或“更新”,这些方法将该实体标记为“已删除”或“已修改”状态。我在物业上使用 DI。我不喜欢每次都指定服务,也不喜欢 2 构造函数。
  • 示例 2:至少但并非最不重要的是,您为什么使用 dbContext.AspNetUsers 而不是 dbSet.Set&lt;AspNetUser&gt;()
  • @ImFlash - 您可以在DbContext 类型上创建DbSet&lt;AspNetUser&gt; 类型的属性,或者您可以在通用DbContext 上使用通用Set&lt;AspNetUser&gt;,选择权在您手中这不是正确的答案。
  • 好的。谢谢!我将使用更多接口,并将“异步”放在我的异步方法中。
【解决方案2】:

坦率地说,这可能不是这里的“最佳”答案,我正在投票给其他人。

因为这对于 cmets 来说有点“长”,所以我发布的答案是“什么”而不是“如何”,就像 Igor 所做的那样——那里有好东西!

这在这方面会有点笼统。

测试我所说的“快乐路径”,即有效的 GUID(在您的实例中)得到有效的东西。

也许是字符串中的有效内容:

Guid g = new Guid("11223344-5566-7788-99AA-BBCCDDEEFF00");

测试一些无效的东西(例如不存在)

Guid g = new Guid(someinvalidthingthatparses);

新:

Guid g = Guid.NewGuid();

全为 0,即Guid.Empty

测试未启动的值

在一个完美的世界中,其中一些不应该到达被测试的方法,但如果他们这样做了怎么办?它有什么反应?

为了进一步推断,我离题了,但是如果您正在测试例如传递整数为 Int32 的东西,它可能只接受正值测试边界,即 0、1、-1、Int32.MaxValueInt32.MinValue @ 987654328@Int32.MinValue + 1,开始思考(就像你正在做的那样)如何打破它。如果您稍后遇到故障情况,请为此添加一个单元测试。如果您传递的东西将在某个地方的回购中,还要考虑它的限制 - 例如日期最小值/最大值在 sql 与 C# 以及日期与 DateTime 等方面不同。BeginDate/EndDate - 是否包含等等。

现在每个“失败”点都应该做点什么,调用者/API 代码是如何处理的?如果抛出错误,它如何响应?

【讨论】:

  • 我的 Api 将该 null 读取为“男士,此用户不存在。将此作为错误返回给客户端并在客户端处理”。感谢你付出的努力!我喜欢这条“幸福之路”。
猜你喜欢
  • 2012-10-11
  • 1970-01-01
  • 2018-02-11
  • 1970-01-01
  • 1970-01-01
  • 2023-03-25
  • 2023-03-08
  • 2022-10-23
  • 2019-11-17
相关资源
最近更新 更多