作为通用原则进行测试
我们已经庆祝千年了将近25年,并且测试才刚刚进入我们的生活...很难说服新手开发人员在他们的工作中使用这种惊人的技术...我们对开发人员的看法,仅仅是凡人,而且并非总是可以理解测试是基础可持续发展的系统! 说服售货员测试新产品并不意味着要吃新产品是多么困难! 即使是经验丰富的保安人员,显然也采用老式的方法-他们试图追赶并选择测试。 而且您不会向他们证明,如果主上帝本人不鄙视在其工作中使用TDD(请记住大洪水),那么正如他们所说,上帝本人已下令...
离婚人数在增加-为什么? 是的,都一样! TDD! 先测试-然后结婚! 不,穿着宽松的羊皮大衣的轻信小矮人,热衷于色情广告,正把他们的年轻妻子推向生产...
好吧,我们与您一起进行另一项测试,首先是测试-然后是其他一切!
我通过步态认出测试员...
因此,当我开始编写下一个代码优先的数据库时,我想到了,为什么不直接在VisualStudio内置的测试中对DAL层进行自动测试呢?
而我做到了! 对EntityFramework透明,没有任何掩饰,也没有伪造物品的欺诈行为。 谁在乎-揭露VS,穿上像测试员一样的衣服,走吧! (我总是打扮得像个测试员)
对所有人说谎,这是测试!
生活案例:使用以下代码处理了一个项目:
ObjectLink link = this.ObjectLinks.ToList().Where(x => x.SpotCode.ToLowerInvariant() == code.ToLowerInvariant()).SingleOrDefault();
该代码未包含在测试中,因为它没有时间-迫切需要启动与营销相关的新功能。 手动检查时一切正常,我已经放松了……但是比尔·盖茨却没有引起注意……
那是一个充满诗意的圣彼得堡秋天,雪和雨轻轻抚摸着我的脸,污垢从裤子的腿上快乐地流下来,陌生的女孩笑着穿过铺开的化妆品,路过的卡车倒了……我已经去过我的手指在鼻子里刺了一下,那时它是危险的,没有宣战,之前黎明时分,微软切断了铁丝网,并发布了核心3.0更新。 托管人已经更新,我也进行了更新,为什么要使用旧的托管人-我比托管人还差吗? 我检查了所有类似内容并推出了更新程序……然后我露出了眼睛! 新功能不起作用! 似乎我之前已经测试过了-会发生什么?
这就是发生的事情:旧的Billy决定从LINQ ToLowerInvariant中删除它...现在您需要提前调用它并插入完成的值...如果代码包含测试,在测试时我会立即注意到它。 很好的是,我本人注意到了所有内容,而不必向客户发誓,因为测试人员为羞于脸红而感到羞愧……我必须解决问题并重新进行部署。
设备和材料:
Microsoft VisualStudio 2019
asp.net Core 3.1(我可以在Studio中安装它,如果可以通过安装其他框架项目菜单进行交付)
SQL Server Express(随Studio附带)
Git对Visual Studio的扩展(包括)
通常,在单元测试中,每个测试都应该隔离,并且它们之间的状态不会持久。 因此,我们实际上将获得集成测试,但是我们将为此使用MSTest。 希望他们不要为此而带我们去警察。
在多个版本中,我遇到了使用Mock对象测试数据库的情况。
乍一看,这个想法是好的,直到表之间开始复杂的交互。
然后,设置模拟将比测试自身花费更多。 但实际上它是-Control + C-Control + V! 我们都是已经在模拟层中的EF,数据库,DataAnnotations或FluentAPI副本中注册的数据库约束。 复制有点像破坏模式……是的,公民,破坏……不好!
而且,如果模拟配置很复杂,例如我们在这里的限制中犯了错误,那么事实证明-模拟测试将通过,但是真正的基础上会有错误吗?
这一切都使我感兴趣,因此我决定测试一种新方法。
这个想法一如既往地来自TRIZ:
理想的系统虽然不存在,但其功能得以实现 。 而且我认为我需要在测试中使用数据库本身。
这对我来说是可行的。 我想分享一下,希望有人帮忙。
模拟的缺点:
- 许多预设可以测试
- 测试变得肮脏,很多额外的代码
- 难以测试迁移
- 只有在监督下才能表现良好,实际上会产生未知错误
- 当更改数据库的结构时,您需要不断地进入模拟并在那里进行所有更改
实际测试的优点:
- 该程序的行为与战斗服务器上的行为完全相同
- 测试更简单,您可以以相同的方式在数据库中填充数据,一个接一个地构建它们
- 我们自己可以通过对测试进行编号来调整数据库的清洁度(在MSTest中,它们是按字母顺序执行的)
- 您可以看到执行测试的时间(在真实服务器上会有所不同,但是至少可以看到顺序-更长10倍,两倍),并且您已经可以评估程序的工作效率(有效或无效)
- 可以测试存储过程
我一直在研究这种方法,但这种方法存在一些困难,但是我们将成功解决这些问题,并且我的坚强后盾将与您同在!
走吧
我们创建一个新项目ASP.Net Core 3.1 Web应用程序(模型-视图-控制器),将身份验证更改为单个用户帐户(在应用程序中存储用户帐户),然后单击创建

从现在开始,我将项目快照保存到git中,您可以下载并上传每个分支以进行实验
github.com/3263927/Habr_1快照:Snapshot_0_Project已创建关于仓库即使我一个人工作,我也总是使用存储库-现在它变得非常方便,可以直接内置到Visual Studio中,无需命令行,一切都可以从VS正常运行。 您可以尝试并更改所需的任何内容,然后始终可以修复提交回滚或切换到旧分支。 我建议大家节省很多时间和精力。 并免费与github集成。 没错,几年前那里有些老兄删除了所有内容...因此,以防万一,我将所有项目放入Dropbox,每周更新一次,并存档所有项目,然后手动将最新版本上传到Google云端硬盘。 好吧,在SD电话上有120个演出,在突然之间突然出现了储备……几个口袋里有副本的闪存驱动器实在是看不见了!
至此,我已经创建了一个存储库,因此现在我需要计划创建新分支的工作。 将来,使用Snapshot关键字可以在出现问题时找到恢复点。
我将直接从VisualStudio在存储库中创建一个新分支,并将其简称为“人才之姐妹”(笑话,Snap_1_DataBases)。
目标:建立有效的联系和基础 。
我们开始建立我们的基地。
我必须马上说,我们将有3个数据库-一个测试(在本地计算机上),另一个生产(在远程服务器上,正在运行),另一个在本地工作(以在DEBUG配置中检查站点的运行状况)。
逻辑是这样的:
- 如果我们想在本地计算机上运行该站点并查看其工作方式,那么Habr1_Local将为我们工作
- 如果我们将代码投入生产,那么Habr1_Production将起作用
- 当我们的测试基础架构开始测试时,它必须找到Habr1_Test基础并运行它
但是,我们有一个矛盾-只有两种配置,即Debug和Release。 这仍然是一个问题,但是它将得到解决。
因此,我们创建了一个最低限度的程序-首先,我们只是检查是否至少有一个数据库对我们有用。 让我们用Visual Studio本身创建它吧!
打开appsettings.json文件
有以下几行:
"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-WebApp-[- , ];Trusted_Connection=True;MultipleActiveResultSets=true" },
在那里,您需要输入正确的连接字符串名称,服务器名称和数据库名称。 我必须马上说在生产中使用其他连接,但是现在我们不需要了。 我们的任务是创建两个数据库(本地数据库和测试数据库,生产只是一个示例-将在发布配置中使用。然后可以用运行中的远程数据库代替)。
为什么需要这个?
Visual Studio配置允许您通过在Visual Studio面板中切换配置来更改某些设置:

通常,开发环境只声明一个DEBUG常量,然后可以从代码中的任何位置读取该常量,并了解我们当前所处的配置,哪个配置处于活动状态。
远程调试器并非始终可用,例如在托管主机上。 我们可以在本地运行asp.net服务器,然后将数据库连接到远程服务器,然后可以看到生产中的所有错误。 在发行版配置中,我们将执行此操作,并且调试配置将与我们的本地数据库一起使用。 而且出于安全原因,我们不会进行测试配置-为了避免意外删除任何数据,而忘记切换配置
因此,我们开始更改连接字符串。
服务器名称-您可以在选项卡视图中看到它-> SQL Server对象资源管理器

(我会谨慎删除计算机的名称,否则,您将通过IP计算我的计算机并输入一些内容)。
所以我有这个(localdb)\ ProjectsV13。 我不知道为什么,在安装过程中我调用了SQL。
这意味着我们的连接字符串正在变成
"DefaultConnection": "Server=(localdb)\\ProjectsV13;Database=Habr1_Local; Trusted_Connection=True;MultipleActiveResultSets=true"
您可能有所不同,但只有ProjectV13。 其余的应该这样保留。
将DefaultConnection更改为Habr1_Local
原来是这样的:
"ConnectionStrings": { "Habr1_Local": "Server=(localdb)\\ProjectsV13;Database=Habr1_Local;Trusted_Connection=True;MultipleActiveResultSets=true" },
现在,您需要转到Startup.cs文件,并在其中用Habr1_Local替换DefaultConnection:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
变成
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("Habr1_Local")));
我们在Debug配置中启动我们的项目,浏览器打开,我们看到第一页,单击Login按钮,在此处输入任何有效的电子邮件(它将不向您发送字母,它只是验证格式),然后单击Login-,我们看到此屏幕:

单击“应用迁移”,我们等待等到蓝色按钮右侧出现确认迁移已通过且您需要刷新页面,更新,单击“确认”以重新发送数据并显示“登录失败”的屏幕时,确认:

如果可以看到这样的屏幕,则一切正常-一旦请求通过并返回了无效的登录尝试,那么就没有数据库错误,但是根本找不到这样的用户。
关于迁移的一点:这是一个复杂的工具,在本文中几乎没有必要触摸它,但是您可以稍微触摸一下。
例如,您需要将数据库的状态更改为特定状态-您可以为此创建数据库快照,或为每种状态创建多个快照,然后以编程方式和使用特殊命令启用/禁用这些映像。
或者,当您在本地计算机上进行开发时,然后需要将服务器上数据库的新状态与本地服务器上的新状态同步,将生产服务器更新为其本地数据库的状态并自动执行-您还可以应用迁移。 这实际上是这个蓝色按钮。 该库知道其状态与代码状态不同,并正在尝试同步这些状态。 为此,将在数据库中创建一个特殊表,其中包含数据库结构的编码状态。
不幸的是,该工具由于没有可视界面而变得复杂,因此您需要从命令行使用它。 当您需要通过Git传递状态时,使用迁移非常方便-只需创建数据库快照(如C#文件)即可。 但是此工具有一个危险-如果配置不正确,它可能会擦除数据库中的数据,因此应谨慎使用。
检查数据库的可用性-Visual Studio应该已经创建了它

如果没有数据库,那就出了问题–要么没有安装SQL Server,要么总的来说是其他什么,就像在开玩笑说程序员是否是医生一样:“医生,我的腿很痛……-好吧,不是我知道,我有一条相同的腿,没有受伤!”
此时,我再创建两个连接字符串,appsettings.json采用以下形式:
"ConnectionStrings": { "Habr1_Local": "Server=(localdb)\\ProjectsV13;Database=Habr1_Local;Trusted_Connection=True;MultipleActiveResultSets=true", "Habr1_Test": "Server=(localdb)\\ProjectsV13;Database=Habr1_Test;Trusted_Connection=True;MultipleActiveResultSets=true", "Habr1_Production": "Server=(localdb)\\ProjectsV13;Database=Habr1_Production;Trusted_Connection=True;MultipleActiveResultSets=true" },
我进行提交,并将以下快照放入存储库中:
快照:Snap_1_DataBases创建一个新分支,Snap_2_Configurations
目标:创建工作配置在切换配置时,我们可以从程序中的任何地方考虑当前的配置(实际上,不是任何配置,在View中都无法使用-我们需要做一个特殊的功能,但这对这个项目并不重要):
#if DEBUG DEBUG #else RELEASE ( DEBUG ) #endif
打开Startup.cs文件,并将ConfigureServices方法转换为此:
public void ConfigureServices(IServiceCollection services) { String ConnStr = ""; #if DEBUG ConnStr = "Habr1_Local"; #else ConnStr = "Habr1_Production"; #endif services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString(ConnStr))); services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddControllersWithViews(); services.AddRazorPages(); }
如您所见,我们用变量替换了Habr1_Local,现在根据配置,连接字符串将为Habr1_Local或Habr1_Production
现在,您可以启动项目并检查如何根据配置创建数据库
我们在面板上选择DEBUG,开始,登录,应用迁移,并检查数据库是否已创建(Habr1_Local)
我们停止项目,选择发布配置,开始,登录,应用迁移,检查数据库是否已创建-我们有2个基础。
做完了!
快照:Snap_3_HabrDB目的:创建一个单独的数据库项目,然后可以在不同的项目中使用为什么要一个单独的项目?
单个项目具有以下优点:
- 它们可以在其他项目中使用。
- 如果没有更改,它们不会重新编译,这意味着总的编译时间减少了
- 单个项目更易于测试。
因此,解决方案上的右键-添加->新解决方案文件夹,将其命名为DB。
然后,右键单击已创建的文件夹-添加新项目-> .net standard,名称为HabrDB。
由于某种原因,我将其创建为.net standard 2.0,因此需要将其更改为2.1
(在创建它提供物理路径时,也应使其位于DB文件夹中,并且也应位于物理位置)。
对我来说看起来像这样:

因此,我们在项目中有一些ApplicationDBContext,并且创建了另一个我们自己的? 他们会互相冲突吗? 现在,我们将与他们成为朋友。 对于同一个数据库,我们将有两个不同的上下文,它们不会通过实体框架相交。 我们将为他们提供不同的架构名称:默认情况下,一个将保留为dbo,另一个将为“ habr”。
如果通过组合的根部连接此类上下文,则可以几乎透明地在其他项目中回收它们。 (例如,仓库环境和员工环境)
还有一个构架时刻,有时您需要向用户添加一些属性,这并不直接适用于本文的主题,但是我们只是想知道如何做到这一点。 另外,我们将能够彼此独立地创建和删除上下文。 一个好主意是将安全性表与个人数据分开,并在数据库级别对其进行加密(我们在此项目中不会这样做,但是总的来说,有时这是必需的,包括法律规定)。
是的,这样测试起来更容易,您不能一次创建所有表,而只能一次创建测试给定上下文所需的表。
我创建一个新的项目快照-阶段4。
此阶段的目标是:- 将标准用户更改为高级
- 更改Srartup.cs文件中的用户
- 在LoginPartial和ViewImports中更改用户
- 创建新的迁移以自动以新格式创建数据库
因此,我们将ApplicationDBContext类从WebApp项目转移到HabrDB。
它不是可移植的,只是被复制了。 我们从WebApp中将其删除,从HabrDB项目中将其打开,并将其命名空间更改为HabrDB,会出现很多错误。
是的,在这个项目中没有必要的软件包,现在我们将交付它们。
通过nuget,在HabrDB项目中,您需要安装Microsoft.AspNetCore.Identity.EntityFrameworkCore。

我们单击灯泡,它为我们提供了安装最新版本的灯泡。
最终的SecurityDBContext文件(还必须重命名)采用以下形式:
using System; using System.Collections.Generic; using System.Text; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace HabrDB { public class SecurityDBContext : IdentityDbContext { public SecurityDBContext(DbContextOptions<SecurityDBContext> options) : base(options) { } } }
组装之后,我们会
仔细处理有错误
的文件 ,这是正确的-我们删除了ApplicationDBContext并将其替换为SecurityDBContext。 现在,您需要使用SecurityDBContext替换整个项目中到ApplicationDBContext的所有链接。
此后,在我的项目中,WebApp引用上出现了黄色三角形,表明某些链接无法正常工作。 我清理了项目(构建->干净的解决方案),关闭了项目,从项目文件夹中删除了所有的Debug,Release,Obj和Bin目录,此后,我再次打开了项目,一段时间后,我在Internet上搜索了必要的链接并加载了它们,并三角形消失了-一切都很好。
现在,从WebApp项目中删除Data文件夹,在SQL Server Object Explorer窗口中删除我们的数据库(Habr1_Local和Habr1_Production)并运行该项目。 我们尝试登录-现在提供了应用迁移的提示,而不是提供错误的提示。

没错,我们删除了所有迁移所在的Data文件夹,现在框架不知道该怎么做。 但这太酷了吗? 为什么呢? 然后,我们现在将扩展用户类。
向HabrDB项目添加一个新文件:
ApplicationUser.cs
我们从IdentityUser继承它
using Microsoft.AspNetCore.Identity; using System; using System.Collections.Generic; using System.Text; namespace HabrDB { public class ApplicationUser:IdentityUser { public String NickName { get; set; } public DateTime BirthDate { get; set; } public String PassportNumber { get; set; } } }
在类头的SecurityDBContext文件中添加:
public class SecurityDBContext : IdentityDbContext<ApplicationUser>
这是必需的,以便EntityFramework知道现在创建数据库时,您需要使用AppllicationUser类中的高级用户模型,而不是标准模型。
Startup.cs文件中的ConfigureServices方法采用以下形式:
public void ConfigureServices(IServiceCollection services) { String ConnStr = ""; #if DEBUG ConnStr = "Habr1_Local"; #else ConnStr = "Habr1_Production"; #endif services.AddDbContext<SecurityDBContext>(options => options.UseSqlServer( Configuration.GetConnectionString(ConnStr))); services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<SecurityDBContext>(); services.AddControllersWithViews(); services.AddRazorPages(); }
(用IdentityUser替换IdentityUser)
在_ViewImports.cshtml文件中,添加以下行
使用 HabrDB
现在,所有视图都将看到带有基础的项目,并且您无需在视图开始时
使用 HabrDB进行编写。
在_LoginPartial.cshtml文件中,将所有IdentityUser更改为ApplicationUser。
使用 Microsoft.AspNetCore.Identity
注入 SignInManager SignInManager
注入 UserManager UserManager
以防万一,我们正在编译项目,以便知道我们没有错误,并且我们也没有忘记在任何地方替换任何东西。
现在进行迁移。
您需要打开Package Manager控制台,选择DB / HabrDB项目并对其进行写入
add-migration initial
这是我得到的:

迁移爸爸出现在HabrDB项目中,并且其中包含文件,可以让我们自动创建数据库,但现在有了我们的其他字段-NickName,BirthDate,PassportNumber。
让我们尝试一下它的工作方式-让我们运行并尝试登录(不要注册,您必须在那输入复杂的密码):

我被提议进行迁移,并且我同意-这是我们的基础:

在这个阶段,一切
快照:Snap_4_Security已准备就绪
创建第五张照片。
目标:- 使测试连接字符串起作用
- 创建一个数据库测试项目
- 使测试项目创建基础并测试有用的东西
我们右键单击DB爸爸,创建一个新项目-MSTest .net core。
将这个项目中的唯一文件重命名为DBTest并认为...
此外,这将是困难的。问题
我们如何创建一个保证与测试数据库通信的上下文,即,不仅使用来自另一个项目(WebApp)的ConnectionString,还可以使用某种方式与Release / Debug配置连接?..可以创建一个新的配置,例如Test ?不,这是潜在的数据丢失-某种程度上我们忘记了忘记忘记从测试配置切换,单击运行所有测试按钮-然后删除整个数据库...不,此选项不起作用...所以我们将清楚地创建一个测试上下文!打开HabrDBContext文件并将其内容更改为: using System; using System.IO; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace HabrDB { public class HabrDBContext:DbContext { public String ConnectionString = ""; public IConfigurationRoot Configuration { get; set; } public HabrDBContext CreateTestContext() { DirectoryInfo info = new DirectoryInfo(Directory.GetCurrentDirectory()); DirectoryInfo temp = info.Parent.Parent.Parent.Parent; String CurDir = Path.Combine(temp.ToString(), "WebApp"); String ConnStr = "Habr1_Test"; Configuration = new ConfigurationBuilder().SetBasePath(CurDir).AddJsonFile("appsettings.json").Build(); var builder = new DbContextOptionsBuilder<HabrDBContext>(); var connectionString = Configuration.GetConnectionString(ConnStr); builder.UseSqlServer(connectionString); ConnectionString = connectionString; return this; } } }
通过Nuget,将以下库添加到数据库: Microsoft.AspNetCore.Identity.EntityFrameworkCore Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.SqlServer Microsoft.Extensions.Configuration.FileExtensions Microsoft.Extensions.Configuration.Json
CreateTestContext方法只能返回一个名为Habr1_Test的连接字符串。您需要将其带入另一个项目。为了仅通过一项设置就不会通过引用连接项目,我们要求构建器根据给定的connectionString创建选项,为此,我们从测试项目的编译目录到项目根目录遍历目录链,从各个部分收集WebApp项目的路径,请为我们添加配置文件appsettings.json(用于存储我们的设置),然后将其编译为配置。从此配置进一步,我们采用connectionString并记住它(稍后我们将需要它)。在大型项目中,您可以将设置移动到带有字符串,DLL或用于通过缓存访问设置数据的对象的单独项目中。现在,一个简单的方法就足够了。为什么这样
您可以将测试ConnectionString直接放入测试项目中并使用它初始化数据库,也可以在数据库中创建测试ConnectionString。但是,那将是一件令人不愉快的事情:所有连接字符串将存储在不同的位置。我一个人知道自己的头上有很多漏洞,例如,当数据库或服务器的名称更改时,我会忘记更改某些内容,因此我将所有连接字符串都放在一个位置,所以我可以这样做。现在,通过更改配置文件appsettings.json,您可以管理所有连接。现在,让我们尝试对基础进行一些有用的操作,例如创建一些内容。在HabrDB项目中,我创建了DBClasses文件夹,那里只有数据库表类。我们首先处理代码,如果我能想象自己在做什么,通常对我来说更方便。创建一个这样的表: using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text; namespace HabrDB.DBClasses { [Table("Phones", Schema ="Habr")] public class Phone { [Key] public int Id { get; set; } public String Model { get; set; } public DateTime DayZero { get; set; } } }
为了美观,我们将我的班级称为“电话”,而数据库中的表应采用复数形式:“电话”。因此,我在Table属性中指示要为我的表命名的名称以及应在数据库中使用的命名空间(在SQL中称为Schema)。现在还有另一个美学问题:在数据库类中,我们将拥有一堆不同的基础结构方法,然后在堆中又有了另一个不同的表,所有这些都可以创建局部类。在HabrDBContext.cs文件中的单词类之后添加单词partial-像这样: public partial class HabrDBContext:DbContext
创建HabrDBContext.cs文件的副本-只需在文件上按CTRL + C-CTRL + V,创建它的副本,将源文件的名称更改为HabrDBContext_Infrastructure.cs,将新文件名更改为HabrDBContext_Data.cs在新文件中,编写: using HabrDB.DBClasses; using Microsoft.EntityFrameworkCore; namespace HabrDB { public partial class HabrDBContext:DbContext { public DbSet<Phone> Phones { get; set; } } }
现在很漂亮-我们在一个地方处理数据,在另一个地方处理基础架构。文件是不同的,但是类是相同的-构建项目时,环境本身会将其从多个组装成一个。好吧,尝试一下!将我们唯一的测试类中的代码替换为: using HabrDB; using HabrDB.DBClasses; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; namespace DBTest { [TestClass] public class DBTest { [TestMethod] public void TestMethod1() { String PhoneName = "Nokia"; DateTime now = DateTime.Now; HabrDBContext db = new HabrDBContext().CreateTestContext(); db.Database.EnsureCreated(); List<Phone> Phones = db.Phones.ToList(); Assert.AreEqual(0, Phones.Count); Phone ph = new Phone(); ph.Model = PhoneName; ph.DayZero = now; db.Phones.Add(ph); db.SaveChanges(); Phone ph1 = db.Phones.Single(); Assert.AreEqual(PhoneName, ph1.Model); Assert.AreEqual(now, ph1.DayZero); } } }
单击“测试资源管理器”选项卡上的播放按钮(或单击“测试”->“运行所有测试”),然后...
错误!这是那些。我们读了他们写给我们的东西: Message: Test method DBTest.DBTest.TestMethod1 threw exception: System.InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions<TContext> object in its constructor and passes it to the base constructor for DbContext.
用英语胡说八道...好吧,我们会随机采取行动!可以将此函数复制到文件HabrDBContext_Infrastructure.cs吗?试试吧! protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { String ConnStr = ""; if (Configuration == null) { #if DEBUG ConnStr = "Habr1_Local"; #else ConnStr= "Habr1_Production"; #endif Configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json").Build(); ConnectionString = Configuration.GetConnectionString(ConnStr); } optionsBuilder.UseSqlServer(ConnectionString); }
我们开始...那是幸运的!一个新的基地已经创建并在其中-我们的桌子!
为什么这样如果在OnConfiguring和CreateTestContext函数上放置几个断点,您将看到首先调用CreateTestContext方法并将连接字符串保存在ConnectionString对象中。一切似乎都还可以。但是随后有人尝试调用OnConfiguring ...是谁?让我们看一下调用堆栈-是的,这是测试中的db.Database.EnsureCreated()行!事实是我们还没有这样的基础-它的确保创建方法创建了它。但是,此方法不再使用参数,并且必须以某种方式在调用构造函数和确保创建之间保留上下文。此外,当我们从项目本身使用此上下文时(例如,不在测试中,而在网站上),各种中间件,DI和其他吸引人的机制也将尝试调用它,因此我们将预见所有内容-无论谁调用我们的数据库,如果他想致电OnConfiguring-他将有这样的机会。我们已经提供了一切。再次运行测试-并...再次出错?数据库已经存在,数据就在...怎么了?基础是一个持久的对象,即使我们重新启动项目也可以保留……现在真正困难的部分开始了。如何删除所有这些表?如何删除数据,重置所有索引?快照:Snap_5_ContextCrafting已经准备就绪,首先,编写DAL-数据访问层。创建HabrDBContext_Data.cs文件的副本,将其命名为HabrDBContext_DAL.cs并写入: using HabrDB.DBClasses; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Threading.Tasks; namespace HabrDB { public partial class HabrDBContext:DbContext { public async Task<int> AddPhone(Phone ph) { this.Phones.Add(ph); int res = await this.SaveChangesAsync(); return res; } public async Task<List<Phone>> GetAllPhones() { List<Phone> phones = await this.Phones.ToListAsync(); return phones; } } }
这是对我们数据的包装。如果我在访问数据库的界面中进行了某些更改,以处理在我调用它的整个项目中进行的搜索,则我不希望这样做。因此,我们将创建一个抽象层-数据访问层。他将成为站点的基本机制和MVC机制或其他机制之间的中介。而且-巧合? -我们立即有测试对象!使用测试的另一个原因是它们会引发连接性低的解决方案。更改测试中的功能代码-并更改其名称之一。现在我们知道我们正在测试什么!添加电话!我们不会做存储库,对于一个演示项目,这是一个冗余解决方案。通常,您可以通过startup.cs中的DI连接所有内容,但是对于演示项目而言,这太令人困惑了,因此让我们将其保留为这样新的测试功能代码: [TestMethod] public void AddPhone_Test() { String PhoneName = "Nokia"; DateTime now = DateTime.Now; HabrDBContext db = new HabrDBContext().CreateTestContext(); db.Database.EnsureCreated(); List<Phone> Phones = db.GetAllPhones().Result; Assert.AreEqual(0, Phones.Count); Phone ph = new Phone(); ph.Model = PhoneName; ph.DayZero = now; db.AddPhone(ph); Phone ph1 = db.Phones.Single(); Assert.AreEqual(PhoneName, ph1.Model); Assert.AreEqual(now, ph1.DayZero); }
我们将从测试数据库中删除带有电话的线路,然后再次运行测试-它应该可以工作。如果它不起作用,那么您仍然有其他选择。一切对我来说都是绿色的:
什么是db.GetAllPhones()。事实是我们的DAL函数是异步的。但是测试方法本身是普通的,因此无法在其中调用wait。让我们尝试删除数据,使方法异步,然后看看会发生什么。我们的功能已成为异步任务-否则,测试将不会开始,无论何时调用异步方法,我们都需要等待 [TestMethod] public async Task AddPhone_Test() { String PhoneName = "Nokia"; DateTime now = DateTime.Now; HabrDBContext db = new HabrDBContext().CreateTestContext(); db.Database.EnsureCreated(); List<Phone> Phones = await db.GetAllPhones(); Assert.AreEqual(0, Phones.Count); Phone ph = new Phone(); ph.Model = PhoneName; ph.DayZero = now; await db.AddPhone(ph); Phone ph1 = db.Phones.Single(); Assert.AreEqual(PhoneName, ph1.Model); Assert.AreEqual(now, ph1.DayZero); }
重要的是,其中存在异步调用的所有测试函数都将返回Task而不是void-否则测试将不会启动或不会等待异步数据的返回并继续执行,并且会出现错误。因此,或多或少的作品。快照:Snap_6_Dal还有什么,有必要手动删除数据吗?当然不是!我们需要从数据库中删除表的功能...使用db.Database.EnsureDeleted是很酷的...的确有可能!但是更好的是没有必要的……事实是,在这个项目中,我们的基地没有与密码连接。而且,如果数据库与密码相关联,则需要通过SQL Management Studio单独创建它,并且当从db.Database.EnsureDeleted中删除它时,它将连同所有密码,访问权限,用户权限以及框架下次尝试创建它时一起删除,那么就根本无法访问数据库,您将不得不重新配置所有内容。这是第一。其次,最好不要做不必要的工作,因此数据库测试比与外部机制无关的常规功能要花费更长的时间,并且最好以所有可能的方式来优化测试时间。第三个:也许在一个测试中,我们将需要删除一些或几次表并重新创建它,而其他表保持不变。让我们尝试调用db.Database.EnsureDeleted函数,按一下括号,看看它接受什么,以及它是否有重载……
是的,不是很多……好吧,让我们编写我们自己的。立即使用右键将新项目添加到解决方案中(在解决方案本身中),在解决方案上添加-> .net standard C#,并将其称为Extensions。检查它是否为2.1版本。接下来,您需要将Extensions项目中的唯一文件重命名为DBContextExtensions.cs并在其中放置以下代码: using Microsoft.EntityFrameworkCore; using System; using System.Linq; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Extensions { public static class DBContextExtensions { public static int EnsureDeleted<TEntity>(this DatabaseFacade db, DbSet<TEntity> set) where TEntity : class { TableDescription Table = GetTableName(set); int res = 0; try { res = db.ExecuteSqlRaw($"DROP TABLE [{Table.Schema}].[{Table.TableName}];"); } catch (Exception) { } return res; } public static TableDescription GetTableName<T>(this DbSet<T> dbSet) where T : class { var dbContext = dbSet.GetDbContext(); var model = dbContext.Model; var entityTypes = model.GetEntityTypes(); var entityType = entityTypes.First(t => t.ClrType == typeof(T)); var tableNameAnnotation = entityType.GetAnnotation("Relational:TableName"); var tableSchemaAnnotation = entityType.GetAnnotation("Relational:Schema"); var tableName = tableNameAnnotation.Value.ToString(); var schemaName = tableSchemaAnnotation.Value.ToString(); return new TableDescription { Schema = schemaName, TableName = tableName }; } public static DbContext GetDbContext<T>(this DbSet<T> dbSet) where T : class { var infrastructure = dbSet as IInfrastructure<IServiceProvider>; var serviceProvider = infrastructure.Instance; var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext; return currentDbContext.Context; } } public class TableDescription { public String Schema { get; set; } public String TableName { get; set; } } }
从Nuget添加Microsoft.EntityFrameworkCore,另一个包Microsoft.EntityFrameworkCore.RelationalExtensions是一种非常方便的机制。使用它,您可以为类对象添加其他功能,即我们在第一个参数类型之前的this关键字。 public static int EnsureDeleted<TEntity>(this DatabaseFacade db, DbSet<TEntity> set) where TEntity : class
表示可扩展类型。这意味着在我们编写完此代码之后,DatabaseFacade对象将具有一个新方法-具有参数的ConfirmDeleted,这将是我们刚编写的函数!其中TEntity:class的末尾表示TEntity具有约束-使用限制,并且如果我们尝试不按类而是按其他方式将其概括,则将出现编译时错误。我预见到您的逻辑问题-为什么会这样?然后,从SecureDeleted调用的GetTableName函数需要此限制。她为什么需要这个限制?然后,从GetTableName调用的什么函数GetDbContext也需要此限制...以及为什么需要此限制?你问高音,你会是正确的...我们尝试使用该行中的GetDbContext方法扩展的dbSet Public static DbContext GetDbContext<T>(this DbSet<T> dbSet) where T : class
,要求T是引用类型,而class只是其中的一种。因此,所有这些困难-对于一个很小但非常有用的功能-不要将字符串值作为参数传递给sureDeleted方法。因为我记性不好。我希望DBContext告诉我它具有哪些表,并且已经从该数据集中计算出上下文,并通过服务提供者将其带入当前上下文,然后从该上下文中获取模型,从中进行类型化,然后查找具有这些类型的类型。与数据集相同(该数据已经在GetTableName中),然后通过注释获取表和架构的名称,将其传递给确保删除(EnsureDeleted),然后该死!同样,没有诸如removetable之类的函数,您必须从字符串构造一些SQL syrniki ...好吧,至少可以通过某种方式实现!而且,确保函数删除是例外,因此您无法考虑删除表的过程,如果那里有任何相关数据,则在删除相关表之后,立即尝试再次删除该表,不要打扰。将方法添加到测试中(最后) [TestMethod] public void DeleteTable_Test() { HabrDBContext db = new HabrDBContext().CreateTestContext(); db.Database.EnsureDeleted(db.Phones); }
运行,检查,呼气...再次运行-应该可以。现在,每次运行测试时,都会创建必要的表,然后将其删除。快照:Snap_7_Extensions通往新的痔疮视野但是,存在一个问题-HabrDBContext对象对我们而言不是持久性的(在每个测试方法中创建)。(是的,要隔离Kagbe单元测试的想法。我们正在努力打破它!警察看不见是很好的……)也就是说,在每种测试方法中,都会重新创建上下文,并且我们不能在函数之间共享该对象这样它对所有人都是通用的,并且为所有测试创建一次。不能吗 好吧,好的,我们将在每个函数中编写代码。 HabrDBContext db = new HabrDBContext().CreateTestContext(); db.Database.EnsureCreated();
并调用最后一个方法 HabrDBContext db = new HabrDBContext().CreateTestContext(); db.Database.EnsureDeleted(db.Phones);
以及其他一些桌子...不是很好,所以我们将考虑如何解决这个问题...由于我们的穿着像测试员一样,我们需要匹配图像-我们将寻求解决方案!类具有如此有趣的功能-静态成员。就像您想象一个图形和从该图形创建的对象一样,事实证明静态成员是不是该图形创建的对象所固有的成员,而是该图形本身。这已经很有趣了...现在让我们尝试!我们的测试类称为DBTest,为其创建一个静态对象-db using HabrDB; using HabrDB.DBClasses; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Extensions; namespace DBTest { [TestClass] public class DBTest { public static HabrDBContext db; [TestMethod] public void AA0_init() { db = new HabrDBContext().CreateTestContext(); db.Database.EnsureCreated(); } [TestMethod] public async Task AddPhone_Test() { String PhoneName = "Nokia"; DateTime now = DateTime.Now; List<Phone> Phones = await db.GetAllPhones(); Assert.AreEqual(0, Phones.Count); Phone ph = new Phone(); ph.Model = PhoneName; ph.DayZero = now; await db.AddPhone(ph); Phone ph1 = db.Phones.Single(); Assert.AreEqual(PhoneName, ph1.Model); Assert.AreEqual(now, ph1.DayZero); } [TestMethod] public void DeleteTable_Test() { db.Database.EnsureDeleted(db.Phones); } } }
有效!但是对这样的数字方法有些愚蠢。有解决办法!特殊的测试属性!在测试项目中,创建一个名为DBTestBase的新类,并从中继承我们的DBTest类。您需要从DBTest中除去以下所有内容: using HabrDB; using HabrDB.DBClasses; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Extensions; namespace DBTest { [TestClass] public class DBTest:DBTestBase { [TestMethod] public async Task AddPhone_Test() { String PhoneName = "Nokia"; DateTime now = DateTime.Now; List<Phone> Phones = await db.GetAllPhones(); Assert.AreEqual(0, Phones.Count); Phone ph = new Phone(); ph.Model = PhoneName; ph.DayZero = now; await db.AddPhone(ph); Phone ph1 = db.Phones.Single(); Assert.AreEqual(PhoneName, ph1.Model); Assert.AreEqual(now, ph1.DayZero); } [ClassCleanup] public static void DeleteTable() { db.Database.EnsureDeleted(db.Phones); } } }
DBTestBase类的内容: using HabrDB; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace DBTest { [TestClass] public class DBTestBase { public static HabrDBContext db{ get; set; }
现在一切都清楚了!我们的数据库是作为DBTestBase类的静态部分创建的,因此从它继承的所有类都可以访问该数据库。这意味着数据库仅创建一次-开始测试时。将[ClassCleanup]属性添加到任何类的任何方法中-并获得“更干净”的测试-这样的函数将在完成该类的所有测试之后执行某些操作。例如,它将删除此测试中创建的所有表,而无需触及数据库本身。由于所有这些特殊功能均已从指标中排除,因此我们可以看到我们的功能正常工作的时间-我们添加了一些左对象,并且选择功能的工作时间急剧增加-这意味着我们的DAL功能有问题,并且如果您测试一个功能在一种测试方法中,将立即清楚问题出在哪里。您也可以根据功能创建播放列表。这对于例如调用数据准备功能(仅用于测试)是必需的。然后,DAL函数将更新此数据,然后再更新另一个DAL函数,例如,它会考虑某些问题,或从该数据显示报告,然后删除该数据。因此,我们可以从真实的业务流程中模仿一些用户操作。要记住的主要事情是,除了这些测试属性外,还按字母顺序调用测试。因此,如果您需要某种静态序列,则应将其命名为:T1_AddPhone_Test,T2_RemovePhone_Test等。好吧,现在您不必担心我们的数据库-所有内容都将进行完整测试!现在该睡觉了!我还有一个女孩在那里没有测试...成功测试!再见!git存储库项目:https : //github.com/3263927/Habr_1如果对此主题很有趣,请在评论中写下评论,然后我写一篇关于测试身份,角色,声明,3D身份验证并编写我的TypeFilterAttribute的帖子(因为标准的一个缓存,并且如果您从某个角色中删除某个人,那么他在结婚之前仍将拥有这个角色。直到接受测试,婚姻才会出现!:/)