RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 559591
Accepted
Bald
Bald
Asked:2020-08-26 13:46:39 +0000 UTC2020-08-26 13:46:39 +0000 UTC 2020-08-26 13:46:39 +0000 UTC

如何测试使用数据库的方法?

  • 772

在我的应用程序中,我使用实体框架版本 6.1.3 来处理mssql数据库。

有一个用测试覆盖你的代码的问题。

请告诉我如何测试这个包。

我想到创建一个本地测试数据库,在这个数据库上填充数据和测试方法,通过这种方法,我可以测试数据检索方法和CRUD方法。

我还听说,为了测试使用数据库的方法,您可以创建一个generic存储库public interface IRepository<T> where T: class { } ,并且为了测试,滑动fake一个包含测试数据的存储库。

到目前为止,我更倾向于第一种情况,即 创建一个本地测试数据库,该数据库将受到与真实数据库相同的更改(由于 ef 中的迁移机制)。

告诉我我描述的测试方法的正确性如何?我很想听听其他方法。

c#
  • 4 4 个回答
  • 10 Views

4 个回答

  • Voted
  1. Best Answer
    Denis Bubnov
    2020-08-26T14:36:32Z2020-08-26T14:36:32Z

    测试是研究和测试软件(软件)的过程,它有两个主要目标:确保软件工作并满足要求,以及识别软件行为不正确、不良或不满足的情况最初的要求。


    您正在考虑两个测试选项。为了确定更合适的方法,有必要找出两种方法的优缺点,并根据获得的结果做出选择。我只是给你建议,你可以自己选择。


    选项1

    创建本地测试数据库,该数据库将受到与真实数据库相同的更改(由于 EF 中的迁移机制)

    这种方法可能存在,但看着它,问题就来了,你准备好牺牲这种方法的一些缺点了吗?我将包括的缺点:

    1. 测试直接依赖于数据库中存储的数据
    2. 多个测试的数据交集。当我们模拟错误或预期不正确的行为时,在测试中存在一些情况 - 这是很正常的。但是在这种情况下呢?我们将拥有好的和坏的数据。
    3. 对迁移的依赖。这里有几个小节。第一:如果我们忘记进行迁移,测试将会失败。第二:您必须始终记住,您也需要为测试进行迁移。第三:维护等测试。新程序员会做什么?
    4. 如果数据库突然崩溃怎么办?测试不再有效。
    5. 这种方法不能称为简单,它有其自身的困难。
    6. 我觉得这个也值得入,我在评论里看到:To check the migration, you can always keep a backup of the desired version

    选项 #2

    要测试使用数据库的方法,您可以创建一个通用存储库公共接口 IRepository where T: class { } 并用于测试滑动带有测试数据的假存储库。

    在我看来,这是一个更方便的测试选项。让我们看看为什么:

    1. 我们不依赖于数据库中的数据,因为数据是在测试本身中准备的。
    2. 数据不相交,我们总是知道测试只适用于我们伪造的东西。
    3. 模拟我们预计会出现错误的情况不会破坏我们的测试,也不会导致问题。
    4. 使用方便。您不需要进行迁移。有很多文档将说明使用模拟(假货)的示例。
    5. 测试速度。模拟(假货)在这里有很大的优势。他们工作得更快。

    有人告诉我,这种方法有一个缺点——这是在测试嵌入在数据库中的逻辑:索引、并发访问等。


    选项 #3

    您可以结合这些方法,在两者之间引入一些东西。例如,在开始向数据库写入数据之前——这是数据准备。然后使用我们的数据所在的特定数据库,即一种用于测试的数据库。最主要的是不要忘记自己清理- 删除用于测试的数据。这种方法比较麻烦,也不简单。

    使用这种方法意味着编写一个辅助 bun 来提供帮助。这将帮助您测试 CRUD 方法。但是这里出现了另一个问题:谁来测试辅助实用程序?于是就有了这样的做法。


    那么,还有一个问题:您要测试您的代码还是 EF?

    如果你按照Test Driven Development进行开发,那么使用 fakes 会大大简化开发。

    我不想专门专注于一个选项,我只是想建议你做出一个更正确的选择,并且不会在未来造成问题。这样过了一会儿,您就不会回头看已完成的工作,也不会对自己说“我为什么不走另一条路? ”。

    必须根据手头的任务做出选择。仅使用一种方法不太可能成功——很可能需要使用多种方法。

    • 10
  2. Mark Shevchenko
    2020-08-26T15:42:19Z2020-08-26T15:42:19Z

    您列出的方法指的是不同类型的测试。创建数据库并对其进行查询 - 集成测试。创建数据库、填充数据库、通过测试——所有这些都将花费大量时间,而且您不希望每分钟都运行这些测试。另一方面,这些测试可以包含在构建过​​程中(集成、持续集成)。如果您没有持续集成服务器,请手动运行它们,但频率要低一些。为此,请使用属性标记它们TestCategory,或将它们全部放在同一个程序集中:Visual Studio 将只允许您从单个项目或一个类别运行测试。

    第二个选项是单元测试。它是不断运行的单元测试,当然,它们不应该执行冗长的操作:访问文件、访问网络资源、数据库服务器。正是在这些测试中使用了模拟和存根对象(它们也是假的)。

    所以我的回答是:你必须做两个测试。现在关于如何去做。

    单元测试

    很多时候,程序员会问这个问题——如何对实体框架进行单元测试?答:没办法,Entity Framework上的单元测试是它的开发者写的,你自己写代码测试。编写比您的代码复杂得多的复杂测试也是错误的。

    public class EfUserRepository : IUserRepository
    {
        private readonly IDbContextFactory dbContextFactory;
    
        public EfUserRepository(IDbContextFactory dbContextFactory)
        {
            this.dbContextFactory = dbContextFactory;
        }
    
        public UserData ReadById(int id)
        {
            using (var dbContext = _dbContextFactory.Create())
            {
                return dbContext.Users.Single(u => u.Id == id);
            }
        }
    }
    

    您可以在此代码中真正测试什么?我立即想测试工作Single并确保该方法返回单个响应或抛出异常。但是Single - 不是你的代码,而且,由于某种原因,很难检查它的调用,因为它是一个扩展方法,而不是一个接口方法IQueryable。但是您可以检查该dbContextFactory方法是否会被调用Create并且创建的上下文将具有属性 access Users。这是使用Moq库的简单方法

    public void ReadById_WhenCalled_CallsDbContextFactoryCreate()
    {
        var dbContextFactoryMock = new Mock<IDbContextFactory>();
        var dbContextFactory = dbContextFactoryMock.Object;
    
        var userRepository = new EfUserRepository(dbContextFactory);
    
        var user = userRepository.ReadById(1);
    
        dbContextFactoryMock.Verify(x => x.Create());
    }
    

    集成测试

    在这里你需要一个基本的和真实的请求。在集成测试中,您将代码作为一个整体进行检查——在表中创建一个条目,然后阅读并确保它在那里。

    你的问题是如何填充这个数据库,这里主要有两个答案:你可以单独用测试数据填充数据库并单独编写测试,你可以在每个测试中创建自己的测试数据集(创建必要的记录表)。第二个选项似乎不好——会有很多额外的条目,测试会运行很长时间。但是,如果你的项目很大,这是唯一的解决方案,因为你很快就会感到困惑,因为基础填充会在一个地方,而使用会在其他几个地方。

    但是现在,据我了解,你一个人在做一个项目,十到十五张表?第一个选项就可以了。

    测试后是否需要清理数据库?您可以,但在大多数正在运行的项目中,删除数据会导致问题。通常的解决方案是仍然在单独的数据库上运行测试,甚至可能每次都运行一个新的数据库。

    最好将集成测试附加到宏DEBUG,例如DB_INTEGRATION. 在实体框架迁移(在)文件夹中,有Migrations一个类Configuration有一个Seed. 调用此方法来填充数据。

    internal sealed class Configuration : DbMigrationsConfiguration<DispatcherDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }
    
        protected override void Seed(DbContext context)
        {
            FillTestData(context);
        }
    
        [Conditional("DB_INTEGRATION")]
        private void FillTestData(DbContext context)
        {
            // с помощью context.AddOrUpdate добавляем тестовые записи
            // во все таблицы
    
            context.SaveChanges();
        }
    }
    

    我们插入该方法FillTestData并将属性附加到它Conditional,因此只有在声明宏定义时才会调用该方法并将其插入到程序集中DB_INTEGRATION。

    必须将完全相同的属性添加到将访问此数据的所有集成测试方法。

    [TestMethod]
    [TestCategory("Integration")]
    [Conditional("DB_INTEGRATION")]
    public void UserCreation()
    {
        var dbContextFactory = new DbContextFactory();
        var userRepository = new EfUserRepository(dbContextFactory);
    
        var username = Utils.GenerateRandomString(200);
        var user1 = userRepository.Create(username);
        var user2 = userRepository.ReadById(user1.Id);
    
        Assert.AreEqual(user1.Name, user2.Name);
    }
    

    现在转到测试项目的属性页面,在BuildDB_INTEGRATION选项卡上,在列表中输入Conditional compilation symbols。重新编译项目后,数据库中会出现测试数据,测试面板中会出现测试。在 Test Explorer 左上角按钮Group By中,选择Group By Traits和类别测试Integration将显示在可以运行的单独组中。正常的单元测试将在No Traits组中结束,因此将它们与集成测试分开运行并经常运行很方便。

    集成测试可以快速进行吗?

    现在,如果您真的想对数据库调用进行真正的测试,但要快速工作?是的,这是可能的。

    第二个问题:即使你没有任何外部服务和数据库,单元测试也应该工作,是否可以在没有外部数据库服务器的情况下进行测试?也是的。

    许多现代 DBMS 允许您将数据库存储在内存中。某些 RDBMS 专门设计用于模拟外部服务器的操作,尽管它们内置于您的应用程序中,例如,请参阅 SQLite 和 SQL Server Compact Edition。

    这些测试本身仍然不是单元测试,但在性能上将是单元级的并且可以经常运行。

    开始使用 http://weblogs.asp.net/scottgu/vs-2010-sp1-and-sql-ce https://www.nuget.org/packages/EntityFramework.SqlServerCompact/

    • 4
  3. Pavel Mayorov
    2020-08-26T16:11:59Z2020-08-26T16:11:59Z

    在 EF 上提出一个包装器,以便在测试时不依赖数据库不是最好的主意。因为这个包装器也应该由某人测试:)

    因此,如果您没有单独的 DAL,则需要在单独的测试数据库上进行测试。保持最新不是问题,您可以在测试中创建数据库并滚动迁移。您可以在某些SetUpFixture中执行此操作

    如果您有一个单独的 DAL,那么您需要在测试基础上以相同的方式对其进行测试。但是 DAL 以上的一切都已经可以通过替换 DAL 进行测试。

    PS 如果被测类没有在里面管理连接和事务——你可以在事务里面测试,然后回滚——这样就解决了测试数据库错误堆积的问题。

    • 3
  4. Monk
    2020-08-26T14:07:26Z2020-08-26T14:07:26Z

    只要有可能,完整的测试应该类似于真实应用程序的操作。

    那些。创建一个基础来运行测试,因为它是为实际工作而创建的,它被注入了工作所需的数据(如果需要负载测试,还有测试数据)。此外,相应地,存在对客户端工作的模仿——连接、执行操作、获取结果并检查它们的有效性。

    值得注意的是,一些测试开始在数据中重叠,因此,如果可能,在测试后删除数据是值得的。那些。测试本身为自己创建测试数据,自己处理和检查,然后自己删除,以尽量减少其数据对其他测试的影响。

    这样的测试是什么样的:

    [TestMethod]
    public void RegisterLetter()
    {
      this.Given(_ => this.GivenNewlyLetter())
        .And(_ => this.GivenNewlyDocumentRegister(DocumentFlow.Incoming))
        .When(_ => this.WhenRegisterDocument(this.letter))
        .Then(_ => this.LetterShouldBeRegistred())
        .TearDownWith(_ => this.DeleteLetter())
        .TearDownWith(_ => this.DeleteDocumentRegister())
        .BDDfy();
    }
    

    我们创建一封信,为它创建一本杂志。我们在日志中注册一封信,检查注册是否成功。我们删除了这封信,我们删除了杂志。在任何情况下都会删除实体,即使测试成功,但不会。

    这种方法的另一个重要优点是,如果您没有部署新基地,您会很快发现它。

    还有一个严重的缺点,我还没有找到一个方便的解决方案。如果你没有机会在测试后清理数据库(这是一种无法合法且轻易绕过的业务限制),那么对相同数据进行相关测试就变得更加困难。


    第二种方式测试,通过Mocks/Fakes,会更快,因为不需要敲base,但是从中得到的好处会少,因为与base的连接和EF的一些特定特性可能是错过了。另一方面,如果你想测试应用程序的业务逻辑而不关心数据库中的存储特性,这是一个很好的选择,它更容易部署、运行和获得结果。

    应该记住,这不是测试负载的方法,因为您丢掉了应用程序的整个层。您不能像这样测试复杂的查询,因为 EF 会将它们包装在 SQL 中,在具体化过程中的某个地方崩溃,等等,这里您有一个在内存中工作的假货,不太可能以足够的可靠性模仿任何 ORM。

    实际上,在我看来,这种测试的优势在现实中会大大减弱。


    作为结论——在我看来,测试应该首先是客户端。那些。按照客户端的方式工作——授权、操作、结果、检查、退出。只要客户端界面不变,它就不会同时关心数据库中隐藏了什么,同时它会一次性检查应用程序的所有层。自然地,这里的问题是部署这样的客户端比仅仅为输入参数并获得结果的函数编写测试要困难得多。另一方面,部署这样的客户端通常与部署真实客户端几乎相同,将其统一起来并不是那么困难。

    是的,你不应该想太多自己清理基地的问题。在大多数情况下,部署一个干净的基地并将其抛在身后就足够了。当来自其他测试的数据开始相互影响时,它们要么干扰您的真实客户,要么测试没有正确地将自己描绘成客户。

    • 2

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    如何停止编写糟糕的代码?

    • 3 个回答
  • Marko Smith

    onCreateView 方法重构

    • 1 个回答
  • Marko Smith

    通用还是非通用

    • 2 个回答
  • Marko Smith

    如何访问 jQuery 中的列

    • 1 个回答
  • Marko Smith

    *.tga 文件的组重命名(3620 个)

    • 1 个回答
  • Marko Smith

    内存分配列表C#

    • 1 个回答
  • Marko Smith

    常规赛适度贪婪

    • 1 个回答
  • Marko Smith

    如何制作自己的自动完成/自动更正?

    • 1 个回答
  • Marko Smith

    选择斐波那契数列

    • 2 个回答
  • Marko Smith

    所有 API 版本中的通用权限代码

    • 2 个回答
  • Martin Hope
    jfs *(星号)和 ** 双星号在 Python 中是什么意思? 2020-11-23 05:07:40 +0000 UTC
  • Martin Hope
    hwak 哪个孩子调用了父母的静态方法?还是不可能完成的任务? 2020-11-18 16:30:55 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    Arch ArrayList 与 LinkedList 的区别? 2020-09-20 02:42:49 +0000 UTC
  • Martin Hope
    iluxa1810 哪个更正确使用:if () 或 try-catch? 2020-08-23 18:56:13 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5