2

我正在使用.NetCore2.2Autofac4Dapper开发新的 WebApi 。很少有非常基本的问题,因为这是我的第一个 WebApi 项目。作为这个项目的一部分,我必须编写单元测试和集成测试。

我的问题如下(示例代码如下):

  1. “ Task< IActionResult > ”和“ Task<IEnumerable> ”之间推荐的返回类型是什么?

  2. 推荐对象 我的项目的启动类中的依赖项范围?

  3. 对于这个给定的项目结构,我真的需要 UnitOfWork 吗?

  4. 如果我按照这个设计有什么缺陷?

  5. 有没有更好的方法来设计这个 API?

  6. 作为 TDD,我是否还需要为 API 层(控制器)和基础设施层或 Doman 层(它没有任何逻辑)编写测试用例?

  7. 我必须在控制器单元测试中包含哪些场景?

领域层:

[Table("Movie")]
public class Movie
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID {get;set;}
    public string Title {get;set;}
}

public interface ICommandRepository<T> where T : class
{
    Task CreateAsync(T entity);

    Task UpdateAsync(T entity);

    Task DeleteAsync(T entity);
}

public interface IQueryRepository<T> where T : class
{
    Task<IEnumerable<T>> GetAllMoviesAsync();

    Task<IEnumerable<T>> GetMoviesByTitleAsync(string title);

    Task<T> GetMovieByIDAsync(int id);
}

基础设施层:

public class MovieCommandContext : DbContext
{
    public MovieCommandContext(DbContextOptions<MovieCommandContext> options)
        : base(options)
    {}

    public DbSet<Movie> Movies { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

public class MovieQueryContext : IDisposable
{
    private readonly IDbConnection connection;

    public MovieQueryContext(string connectionString)
    {
        connection = new SqlConnection(connectionString);
    }

    public async Task<IEnumerable<Movie>> GetAllMovies()
    {
        // Use Dapper->QueryAsync
        throw new NotImplementedException();
    }

    ...

    public void Dispose()
    {
        if (connection?.State == ConnectionState.Open)
            connection.Close();
    }
}

public class MovieCommandRepository : ICommandRepository<Movie>
{
    private readonly MovieCommandContext context;

    public MovieCommandRepository(MovieCommandContext dbContext)
    {
        context = dbContext;
    }

    public async Task CreateAsync(Movie movie)
    {
        await context.AddAsync<Movie>(movie);
        await context.SaveChangesAsync();
    }

    public async Task UpdateAsync(Movie movie)
    {
        var entity = context.Attach<Movie>(movie);
        context.Entry<Movie>(movie).State = EntityState.Modified;
        await context.SaveChangesAsync();
    }

    public async Task DeleteAsync(Movie movie)
    {
        context.Remove<Movie>(movie);
        await context.SaveChangesAsync();
    }
}

public class MovieQueryRepository : IQueryRepository<Movie>
{
    private readonly MovieQueryContext context;

    public MovieQueryRepository(MovieQueryContext dbContext)
    {
        context = dbContext;
    }

    public async Task<IEnumerable<Movie>> GetAllMoviesAsync()
    {
        return await context.GetAllMovies();
    }

    public async Task<IEnumerable<Movie>> GetMoviesByTitleAsync(string title)
    {
        return await context.GetMovieByName(title);
    }

    public async Task<Movie> GetMovieByIDAsync(int id)
    {
        return await context.GetMovieByID(id);
    }
}

API层:

[Route("api/sample")]
[ApiController]
public class SampleController : ControllerBase
{
    private readonly ICommandRepository<Movie> movieCommand;
    private readonly IQueryRepository<Movie> movieQuery;

    public SampleController(ICommandRepository<Movie> command, IQueryRepository<Movie> query)
    {
        movieCommand = command;
        movieQuery = query;
    }


    [HttpGet]
    public async Task<IActionResult> GetMoviesAsync()
    {
        try
        {
            var movies = await movieQuery.GetAllMoviesAsync();
            return Ok(movies);
        }
        catch
        {
            // TODO: Logging 
            return BadRequest();
        }
    }

    [Route("{name:alpha}")]
    [HttpGet]
    public async Task<IActionResult> GetMoviesByTitle(string movieTitle)
    {
        try
        {
            var movies = await movieQuery.GetMoviesByTitleAsync(movieTitle);
            return Ok(movies);
        }
        catch
        {
            // TODO: Logging 
            return BadRequest();
        }
    }

    [Route("{movieID:int:min(1)}")]
    [HttpGet]
    public async Task<IActionResult> GetMovieByID(int movieID)
    {
        try
        {
            var movie = await movieQuery.GetMovieByIDAsync(movieID);
            return Ok(movie);
        }
        catch
        {
            // TODO: Logging 
            return BadRequest();
        }
    }

    [Route("")]
    [HttpDelete("{id:int:min(1)}")]
    public async Task<IActionResult> Delete(int id)
    {
        try
        {
            var movie = await movieQuery.GetMovieByIDAsync(id);

            if (movie == null)
                return BadRequest();

            await movieCommand.DeleteAsync(movie);
            return Ok();
        }
        catch
        {
            // TODO: Logging
            return BadRequest();
        }
    }
}

启动.cs:

private void ConfigureContainer(ContainerBuilder builder)
    {
        var contextOptions = new DbContextOptionsBuilder<MovieCommandContext>()
                            .UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
                            .Options;

        builder.RegisterType<MovieCommandContext>()
            .WithParameter("options", contextOptions);

        builder.RegisterType<MovieQueryContext>()
            .AsSelf()
            .WithParameter("connectionString",Configuration.GetConnectionString("DefaultConnection"));

        builder.RegisterType<MovieCommandRepository>().As<ICommandRepository<Movie>>();
        builder.RegisterType<MovieQueryRepository>().As<IQueryRepository<Movie>>();
    }
4

2 回答 2

0

1. “Task< IActionResult >”和“Task< IEnumerable < Movie >>”之间推荐的返回类型是什么?

尽管 API 允许您使用接口 IActionResult,但我根本不会使用它。为什么?语义,了解真正回报的唯一方法是查看实现。如果返回的是 Task< IEnumerable< Movie>>,那就更清楚了。

如果您需要抛出 BadRequest 或其他 http 代码,请使用 asp.net 管道为您处理。请参阅下面的注释。

当使用任何工具生成此 API 的某种文档时,它无助于隐藏真实结果。

2. 我的项目的启动类中依赖的对象范围?

避免在调用之间共享状态,以避免未来的同步问题,只需坚持每个请求的范围依赖性。如果您有很多请求,这可能是性能问题,您可以稍后随时更改。如果这是一个问题。

3. 对于这个给定的项目结构,我真的需要 UnitOfWork 吗?
4. 如果我按照这个设计有什么缺陷?
5.有没有更好的方法来设计这个API?

希望能回答以上3个问题。我看到的问题是围绕电影模型扩展功能。例如,在 ICommandRepository 上添加第四个操作。

它接缝会垂直生长。如果多个类都实现了这个接口,这只会是一个问题,因为它们都需要改变。(接口隔离原则)

解决这个问题的一种方法是使用中介者模式。您的控制器将接收中介,中介将消息传递给处理它的任何人。使用这种类型的解决方案,您可以为每个操作创建一个类,因此您的系统可以随着新类添加到系统而水平增长。(开闭原则)

随着时间的推移,您会发​​现很多功能可以重用,而添加功能只是配置问题。

6. 作为TDD,我是否也需要为API 层(控制器)和基础设施层或域层(它没有任何逻辑)编写测试用例?

一般来说,测试的想法是测试行为,当 TDDing 这应该是你的心态时。根据我的经验,我发现测试整个行为比测试同一行为的多个部分要好。

在这种情况下,API 层和持久层一样是基础架构的一部分。他们应该有自己的测试,业务规则(应用层)应该有自己的测试。应用层是你想要永远存在的东西。随着技术的出现(windows 表单、web 表单、web api 等),API 将发生变化。关于数据库,您不知道是否要永远坚持使用 EF。

如果域层不提供任何行为,那么就没有什么可测试的了。

7. 我必须在控制器单元测试中包含哪些场景?

我会使用 asp.net TestHost 进行测试:

https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2

测试路由是否正确,测试失败场景和成功场景。

一些注意事项:

  • Controller 中的异常并不意味着 BadRequest。
  • 日志记录是一个跨领域的问题,不要到处都这样做。要么使用 asp.net 管道,要么将这个问题转移到应用层。
  • 看来 MovieQueryRepository 什么都不做,所以你不需要它。
  • 这只是对您的问题的一些评论,还有更多内容。只要记住要保持简单和有条理。

希望它有帮助,让我知道!

于 2019-01-08T10:31:21.667 回答
0

第 1 点:

您应该返回 IActionResult 以返回适当的 Http 响应,而不是返回Task<IEnumerable<Movie>>. 这样您就可以保证 SOLID 原则的 S 和 I

第 2 点和第 3 点:

请参见此处: Entity Framework Core 服务默认生命周期

第 4 点:

IQueryRepository 作为一些不好的方法名称。名称与域概念紧密耦合,它们不应该。您未能通过关注点分离(SOLID 的 S)。ICommandRepository 作为一个 Add 方法,它被暴露给某个控制器并且没有被使用(与 Update 相同)在这里你在接口隔离上失败了。

MovieQueryContext 没有正确实现 IDisposable 模式,请看这里

MovieQueryContext 在初始化方式上与 MovieCommandContext 不同。为什么?您应该尝试在设计类型时保持连贯性,因为它会为您提供可重用性并应用 DRY 原则。

如果对数据库的访问更改为 mongodb,请考虑您需要付出的努力。或者如果对数据库的访问更改为远程服务有多少更改,以及您在哪里进行更改以支持该更改?

如果 Movie 是域类型,则它不应具有任何特定数据库访问权限的属性。尽可能保持它 POCO。

第 5 点:

要设计您的 API,请考虑这篇文章。注入依赖项的方式应该考虑这些对象的生命周期。请记住,在 aspnet.core ApiControllers 中,生命周期是每个请求的。您管理资源以访问数据库的方式应考虑到这一点。

如果您正在考虑 CQRS,控制器应该是不同的。牢记关于这些责任的关注点的分离。一个控制器负责公开一些查询 API,另一个负责处理命令。有很好的框架可以支持 CQRS,请参阅这个scott hanselman 的帖子

约束存在于 Route 属性上而不是 Verbs 上。日志记录和异常处理应该在 ActionAttribute 或某些特定的中间件上完成,因为它们被认为是横切关注点

Delete Action 不符合 Http 协议。请考虑http rfc

GetMoviesByTitle Action 没有 name 参数。

第 6 点:

单元测试应该测试业务逻辑,使用与现有测试相关的值来模拟所有外部依赖项。TDD 方法考虑了 3 个主要步骤(此处了解更多详细信息):

  1. 第一步包括实施单元测试,所以它失败了
  2. 迭代正在测试的方法的实现,直到它成功通过
  3. 改进正在测试的方法的实现

如果您想测试您的 ApiController 是否与所有集成的中间件一起使用,您需要在不使用打开端口的实际服务器的情况下放置该环境。为此,请考虑使用 TestServer(请参阅此处此处

于 2019-01-08T10:32:31.683 回答