十三、理解测试如何工作
在前几章中,我们介绍了如何在 ASP.NET Core 中构建内容。我们知道,我们应该在测试我们的应用之前,我们认为他们完成了。发生的情况是,应用不断发展,过去在某个时间点工作的东西现在可能不再工作了。因此,为了确保这些应用不会在我们身上失败,我们设置了可以自动运行的测试,并检查是否一切都正常工作。
本章将介绍以下主题:
- 单元测试原则
- 使用 xUnit、NUnit 和 MSTest
- 模拟对象
- 断言
- 用户界面测试
- 进行集成测试
在本章中,我们将了解执行这些测试的两种方法,以及它们如何帮助我们确保编写集成良好的代码。
技术要求
为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足所有要求,但您也可以使用 VisualStudio 代码。
开始单元测试
单元测试并不新鲜。从本质上讲,单元测试旨在单独测试系统的一个功能,以证明它能够正常工作。F.I.R.S.T 单元测试原则规定单元测试应如下所示:
- 快速:他们应该快速执行,这意味着他们不应该执行任何复杂或冗长的操作。
- 隔离/独立:单元测试不应依赖于其他系统,应提供独立于任何特定上下文的结果。
- 可重复:如果在实现上没有任何更改,则无论何时执行单元测试,单元测试都应产生相同的结果。
- 自我验证:应该是自给自足的,即不需要任何人工检查或分析。
- 彻底/及时:他们应该涵盖所有重要的东西,即使 100%的代码不需要。
简而言之,单元测试应该运行得很快,这样我们就不必等待很长时间的结果,并且应该进行编码,这样就可以测试基本特性,而不依赖于外部变量。此外,单元测试不应该产生任何副作用,并且应该能够重复它们,并始终获得相同的结果。
有些人甚至主张在实际代码之前就开始实现单元测试。这样做的好处是使代码可测试——毕竟,它的设计考虑到了测试,一旦我们实现了它,我们就已经有了单元测试。这被称为测试驱动开发(TDD。虽然我不是 TDD 的铁杆后卫,但我看到了它的优势。
使用 TDD 的开发通常会经历一个称为红绿重构的循环,这意味着测试首先是红色的(意味着测试失败),然后是绿色的(通过),只有到那时,当一切正常工作时,我们才需要重构代码来改进它。有关 TDD 的更多信息,请访问https://technologyconversations.com/2014/09/30/test-driven-development-tdd 。
我们通常依赖单元测试框架来帮助我们执行这些测试并获得结果。.NET Core 有几种框架,包括:
- MSTest:这是微软自己的测试框架;它是开源的,可在上获得 https://github.com/Microsoft/testfx 。
- xUnit:一个甚至被微软使用的流行框架,可在上找到 https://xunit.github.io 。
- NUnit:最古老的单元测试框架之一,从 Java 的JUnit移植而来,可在上获得 http://nunit.org 。
这些都是开源的,它们的特性是相似的。您可以在找到三个框架的良好比较 https://xunit.github.io/docs/comparisons.html 。
如果您更喜欢从控制台而不是 Visual Studio 启动项目,dotnet
有 MSTest、NUnit 和 xUnit 的模板,请选择一个:
dotnet new mstest
dotnet new xunit
dotnet new nunit
现在让我们来掌握代码。
编写单元测试
在这里,我们将看到如何将一些最流行的单元测试框架与.NET Core 结合使用。这将不是对框架的深入介绍,只是让您开始学习的基础知识。
单元测试是.NETCore 中的一流公民,有自己的项目类型。基本上,单元测试项目使用Microsoft.NET.Sdk
SDK,但必须引用Microsoft.NET.Test.Sdk
。正如我们将看到的,dotnet
工具了解这些项目,并为它们提供了特殊的选项。
首先,我们需要使用一个受支持的框架创建一个单元测试项目。
单元测试框架
有很多单元测试框架,但我选择了最常用的,包括微软。
MSTest
MSTest 是微软自己的测试框架,最近开源。要使用 MSTest,您需要添加对以下 NuGet 包的引用:
-
MSTest.TestFramework
-
MSTest.TestAdapter
-
Microsoft.NET.Test.Sdk
第一个参考是框架本身,第二个是允许 VisualStudio 与 MSTest 交互的参考;是的,所有这些框架都与 VisualStudio 很好地集成!
Visual Studio 为 MSTest 项目提供了一个模板项目,但您也可以使用dotnet
创建一个模板项目:
dotnet new mstest
添加对 web app 项目的引用,并创建一个名为ControllerTests
的类,其内容如下:
[TestClass]
public class ControllerTests
{
[TestMethod]
public void CanExecuteIndex()
{
var controller = new HomeController();
var result = controller.Index();
Assert.IsNotNull(result);
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
}
这是一个简单的单元测试,将检查在HomeController
类上调用Index
方法的结果是否为null
。
注意CanExecuteIndex
方法中的[TestMethod]
属性;这表示此方法包含单元测试,并由 Visual Studio 的测试资源管理器功能捕获:
如果满足以下条件,Visual Studio 将能够找到任何测试方法:
- 它是在非抽象类中声明的。
- 该类用一个
[TestClass]
属性修饰。 - 这是公开的。
- 它具有
[TestMethod]
或[DataRow]
属性(稍后将对此进行详细介绍)。
从这里,您可以运行或调试您的测试;尝试在CanExecuteIndex
方法中放置断点并调试测试。它由 VisualStudio 自动调用,并考虑在没有引发异常的情况下测试是否通过。控制器通常是很好的单元测试候选者,但您也应该对服务和业务对象进行单元测试。请记住,首先关注最关键的类,然后,如果您有资源、时间和开发人员,继续编写不太关键的代码。
除了[TestMethod]
之外,您还可以使用一个或多个[DataRow]
属性来装饰您的单元测试方法。这允许您传递参数的任意值,并返回由单元测试框架自动提供的方法的值:
[TestClass]
public class CalculatorTest
{
[DataTestMethod]
[DataRow(1, 2, 3)]
[DataRow(1, 1, 2)]
public void Calculate(int a, int b, int c)
{
Assert.AreEqual(c, a + b);
}
}
在本例中,我们可以看到我们提供了两组值-1
、2
和3
以及1
、1
和2
。这些都是自动测试的。
如果要在同一类中的任何测试之前或之后执行代码,可以应用以下内容:
[TestClass]
public class MyTests
{
[ClassInitialize]
public void ClassInitialize()
{
//runs before all the tests in the class
}
[ClassCleanuç]
public void ClassCleanup()
{
//runs after all the tests in the class
}
}
或者,对于在每次测试之前和之后运行的代码,应用以下内容:
[TestInitialize]
public void Initialize()
{
//runs before each test
}
[TestCleanup]
public void Cleanup()
{
//runs after each test
}
如果要忽略特定的测试方法,请应用以下方法:
[Ignore("Not implemented yet")]
public void SomeTest()
{
//will be ignored
}
Assert
类提供了许多在单元测试中使用的实用方法:
AreEqual
:要比较的项目相同。AreNotEqual
:要比较的项目不相同。AreNotSame
:要比较的项目没有得到相同的参考。AreSame
:要比较的项目具有相同的参考。Equals
:项目相等。Fail
:断言失败。Inconclusive
:该断言没有结论性。IsFalse
:该条件预计为假。IsInstanceOfType
:实例应为给定类型。IsNotInstanceOfType
:实例不应为给定类型。
Please refer to the MSTest documentation at https://docs.microsoft.com/en-us/visualstudio/test/using-microsoft-visualstudio-testtools-unittesting-members-in-unit-tests?view=vs-2019 for more information.
接下来,我们有 NUnit。
单元测试
NUnit 是最古老的单元测试框架之一。要在代码中使用它,您需要添加以下 NuGet 包作为引用:
-
nunit
-
NUnit3TestAdapter
-
Microsoft.NET.Test.Sdk
同样,第一个是框架本身,第二个是与 VisualStudio 的集成。要创建 NUnit 项目,除了使用 Visual Studio 模板外,还可以使用dotnet
创建一个 NUnit 项目:
dotnet new nunit
添加对 web 应用项目的引用,并将此类类添加到名为ControllerTests.cs
的文件中:
[TestFixture]
public class ControllerTests
{
[Test]
public void CanExecuteIndex()
{
var controller = new HomeController();
var result = controller.Index();
Assert.AreNotEqual(null, result);
}
}
Visual Studio 能够自动查找单元测试,前提是满足以下条件:
- 它们在非抽象类中声明。
- 该类用一个
[TestFixture]
属性修饰。 - 它们是公开的、非抽象的。
- 它们具有
[Test]
或[TestCase]
属性。
[TestCase]
允许我们自动传递多个参数,以及预期的返回值(可选):
public class CalculatorTest
{
[TestCase(1, 1, ExpectedResult = 2)]
public int Add(int x, int y)
{
return x + y;
}
}
注意,在这个示例中,我们甚至不需要指定断言,如果您指定了ExpectedResult
,那么断言将自动推断出来。
现在,如果您希望在代码中的任何测试之前运行某些内容,则需要在代码中包含以下内容:
[SetUpFixture]
public class InitializeTests
{
[OneTimeSetUp]
public void SetUpOnce()
{
//run before all tests have started
}
[OneTimeTearDown]
public void TearDownOnce()
{
//run after all tests have finished
}
}
类和方法的名称是不相关的,但是类需要是公共的,并且有一个公共的无参数构造函数,方法也需要是公共的和非抽象的(可以是静态的或实例的)。请注意类和SetUp
(在前面运行)和TearDown
(在后面运行)方法上的属性。并非所有这些都需要提供,只有一个。
同样,如果希望在每次测试之前运行代码,则需要将方法标记为:
[SetUp]
public void BeforeTest()
{
//runs before every test
}
两者的区别在于标记为[SetUp]
的方法在每次测试之前运行,而[OneTimeSetUp]
和[OneTimeTearDown]
只在每个测试序列(所有测试)中运行一次。
如果出于某种原因(例如,测试失败或尚未完成)希望忽略测试,则可以使用另一个属性对其进行标记:
[Ignored("Not implemented yet")]
public void TestSomething()
{
//will be ignored
}
与其他框架一样,有一个名为Assert
的类,它包含一些 helper 方法:
IsFalse
:给定条件为假。IsInstanceOf
:传递的实例是给定类型的实例。IsNaN
:传递的表达式不是数字。-
IsNotAssignableFrom
:传递的实例不可从给定类型分配。 -
IsNotEmpty
:集合不为空。 IsNotInstanceOf
:传递的实例不是给定类型的实例。IsNotNull
:实例不是null
。IsNull
:实例为null
。IsTrue
:条件为真。
For more information, please consult the NUnit documentation at https://github.com/nunit/docs/wiki/NUnit-Documentation.
接下来是 xUnit。
单元测试
为了使用 xUnit,您需要添加几个 NuGet 包:
xunit
xunit.runner.visualstudio
Microsoft.NET.Test.Sdk
第一个是框架本身,另外两个是 VisualStudio 集成所必需的。VisualStudio2019 甚至提供了 xUnit 测试项目模板,这甚至更好!
让我们创建一个单元测试项目;因为我们将以 ASP.NET Core 功能和.NET Core 应用为目标,所以我们需要创建一个也以.NET Core 应用为目标的单元测试项目-netcoreapp3.0
。如前所述,您可以通过 Visual Studio 或使用dotnet
工具来创建模板项目:
dotnet new xunit
在这个项目中,我们向 web 应用添加一个引用,并创建一个类。我们叫它ControllerTests
。在这个类中,我们添加了以下代码:
public class ControllerTests
{
[Fact]
public void CanExecuteIndex()
{
var controller = new HomeController();
var result = controller.Index();
Assert.NotNull(result);
}
}
这是一个非常简单的测试。我们正在创建一个HomeController
实例,执行其Index
方法,并检查是否未引发异常(隐式,否则测试将失败),以及其结果是否不是null
。
Unlike other frameworks, with xUnit, you do not need to decorate a class that contains unit tests.
Visual Studio 会自动发现单元测试,并在测试资源管理器窗口中显示它们,前提是满足以下条件:
- 它们在非抽象类中声明。
- 它们是公开的。
- 它们具有
[Fact]
或[Theory]
属性。
[Theory]
更有趣,因为您可以为您的测试方法提供参数,xUnit 将负责调用这些参数!你自己看看:
[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 10, 10)]
public void Add(int x, int y, int z)
{
Assert.Equals(x + y, z);
}
这个例子有点简单,但我想你明白了![InlineData]
应具有与其声明的方法一样多的参数。因为我们有两个[InlineData]
属性,所以我们有两个数据集,所以对于其中一个[InlineData]
属性中的每个值,该方法将被调用两次。
或者,如果您想测试动作方法模型,可以使用以下方法:
var controller = new ShoppingController();
var result = controller.ShoppingBag();
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<ShoppingBag>(viewResult.ViewData.Model);
您可以有任意多个测试方法,并且可以从 VisualStudioTestExplorer 窗口运行一个或多个。您的每个方法都应该负责测试一个特性,所以请确保不要忘记这一点!通常,单元测试是根据排列 Act Assert(AAA)设置的,这意味着我们首先设置(排列)对象,然后调用它们上的一些代码(Act),然后检查其结果(Assert)。一定要记住这个助记符!
如果您进行单元测试的类实现了IDisposable
,那么在所有测试结束时将自动调用其Dispose
方法。当然,类的构造函数也将运行,因此它需要是公共的,并且没有参数。
如果您让您的测试类实现IClassFixture<T>
,xUnit 将期望它包含一个公共构造函数,该构造函数接受T
的实例(因此,该实例必须是公共的可实例化类型),并将其实例传递给实现相同接口的所有单元测试类:
public class MyTests : IClassFixture<SharedData>
{
private readonly SharedData _data;
public MyTests(SharedData data)
{
this._data = data;
}
}
最后,如果希望忽略单元测试,只需在[Fact]
或[Theory]
属性上设置Skip
属性:
[Fact(Skip = "Not implemented yet")]
public void TestSomething()
{
//will be ignored
}
xUnitAssert
类中有几种实用程序方法,如果不满足条件,它们将引发异常:
All
:集合中的所有项目都符合给定条件。Collection
:集合中的所有项目都符合所有给定条件。Contains
:集合包含一个给定的项目。DoesNotContain
:集合不包含给定项。DoesNotMatch
:字符串与给定的正则表达式不匹配。-
Empty
:集合为空。 -
EndsWith
:字符串以一些内容结尾。 Equal
:两个集合相等(包含完全相同的元素)。Equals
:两项相等。False
:表示为假。InRange
:可比值在一定范围内。IsAssignableFrom
:对象可从给定类型进行赋值。IsNotType
:对象不是给定类型。IsType
:对象为给定类型。Matches
:字符串与给定的正则表达式匹配。NotEmpty
:集合不为空。NotEqual
:两个对象不相等。NotInRange
:可比值不在范围内。NotNull
:该值不是null
。NotSame
:两个参照物不是同一个对象。NotStrictEqual
:使用默认比较器(Object.Equals
验证两个对象是否不相等。Null
:检查值是否为null
。ProperSubset
:验证一个集合是否是另一个集合的适当子集(包含)。ProperSuperset
:验证一个集合是否是(包含)另一个集合的正确超集。PropertyChanged
/PropertyChangedAsync
:验证属性是否已更改。Raises
/RaisesAsync
:验证某个操作是否引发事件。RaisesAny
/RaisesAnyAsync
:验证某个操作是否引发给定事件之一。Same
:两个引用指向同一个对象。Single
:集合包含且仅包含一项。StartsWith
:验证一个字符串是否以另一个字符串开头。StrictEqual
:使用默认比较器(Object.Equals
验证两个对象是否相等。-
Subset
:验证一个集合是否是另一个集合的子集(包含)。 -
Superset
:验证一个集合是否是另一个集合的超集(包含)。 Throws
/ThrowsAsync
:验证操作是否引发异常。ThrowsAny
/ThrowsAnyAsync
:验证某个操作是否引发给定的异常之一。True
:这句话是真的。
本质上,所有这些方法都是True
的变体;您想断言一个条件是否为真。在单元测试方法中不要有太多断言;确保只测试 essentials,例如,检查方法在测试中是否返回非空或非 null 集合,并让其他测试检查返回值的正确性。如果要测试不同的场景或返回值,请创建另一个单元测试。
For more information, please consult the xUnit documentation at https://xunit.net/#documentation.
现在让我们看看如何使用 xUnit 准备单元测试。
测试设置
本章中的示例都将使用 xUnit 作为单元测试框架。
注入依赖项
它可能并不总是简单的;例如,要测试的类可能包含依赖项。到目前为止,将依赖项注入控制器的最佳方法是通过其控制器。以下是执行日志记录的控制器示例:
ILogger<HomeController> logger = ...;
var controller = new HomeController(logger);
幸运的是,HttpContext
的RequestServices
属性本身是可设置的,这意味着您可以使用所需的服务构建自己的实例。检查以下代码:
var services = new ServiceCollection();
services.AddSingleton<IMyService>(new MyServiceImplementation());
var serviceProvider = services.BuildServiceProvider();
controller.HttpContext.RequestServices = serviceProvider;
如果您的代码依赖于当前正在进行身份验证或拥有某些声明的用户,则需要设置一个HttpContext
对象,您可以这样做:
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "username")
}))
}
};
这样,在控制器内部,HttpContext
和User
属性将被正确初始化。在DefaultHttpContext
类的构造函数中,还可以传递一组特性(即HttpContext.Features
集合):
var features = new FeatureCollection();
features.Set<IMyFeature>(new MyFeatureImplementation());
var ctx = new DefaultHttpContext(features);
通过使用自定义要素集合,可以为许多要素注入值,例如:
Sessions
:ISessionFeature
Cookies
:IRequestCookiesFeature
、IResponseCookiesFeature
Request
:IHttpRequestFeature
Response
:IResponseCookiesFeature
Connections
:IHttpConnectionFeature
Form
:IFormFeature
通过在 features 集合中提供您自己的实现,或者通过将值分配给现有的实现,您可以为测试注入值,以便模拟真实场景。例如,假设您的控制器需要特定的 cookie:
var cookies = new RequestCookieCollection(new Dictionary<string, string> { { "username", "dummy" } });
var features = new FeatureCollection();
features.Set<IRequestCookiesFeature>(new RequestCookiesFeature(cookies));
var context = new DefaultHttpContext(features);
RequestCookieCollection
过去是公开的,但现在是内部的,这意味着要模拟 cookies,我们需要自己实现它们。以下是最简单的实现:
class RequestCookieCollection : IRequestCookieCollection
{
private readonly Dictionary<string, string> _cookies;
public RequestCookieCollection(Dictionary<string, string> cookies)
{
this._cookies = cookies;
}
public string this[string key] => _cookies[key];
public int Count => _cookies.Count;
public ICollection<string> Keys => _cookies.Keys;
public bool ContainsKey(string key)
{
return _cookies.ContainsKey(key);
}
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
{
return _cookies.GetEnumerator();
}
public bool TryGetValue(string key, out string value)
{
return _cookies.TryGetValue(key, out value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
现在,您应该注意,您不能更改ControllerBase
上的HttpContext
对象—它是只读的。然而,事实证明它实际上来自ControllerContext
属性,该属性本身是可设置的。以下是一个完整的示例:
var request = new Dictionary<string, StringValues>
{
{ "email", "rjperes@hotmail.com" },
{ "name", "Ricardo Peres" }
};
var formCollection = new FormCollection(request);
var form = new FormFeature(formCollection);
var features = new FeatureCollection();
features.Set<IFormFeature>(form);
var context = new DefaultHttpContext(features);
var controller = new HomeController();
controller.ControllerContext = new ControllerContext { HttpContext = context };
此示例允许我们设置表单请求的内容,以便可以在控制器内部的单元测试中访问它们,如下所示:
var email = this.Request.Form["email"];
为此,我们必须创建一个表单集合(IFormCollection
和一个功能(IFormFeature
),使用该功能构建一个 HTTP 上下文(HttpContext
),用 HTTP 上下文分配一个控制器上下文(ControllerContext
),并将其分配给我们想要测试的控制器(HomeController
。这样,它的所有内部属性-HttpContext
和Request
都将具有我们作为请求传递的伪值。
依赖性的挑战之一是,因为我们执行的是系统的有限子集,所以可能不容易获得正常运行的对象;我们可能需要用替代品来取代它们。我们现在来看看如何解决这个问题。
嘲笑
模拟、伪造和存根是类似的概念,本质上意味着一个对象被另一个模仿其行为的对象替代。我们为什么要这样做?好的,因为我们是孤立地测试我们的代码,并且我们假设第三方代码按照广告的方式工作,所以我们不关心它,所以我们可以用假人替换这些其他依赖项。
For a comparison of these terms, please refer to https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da.
为此,我们使用模拟框架,对于.NET Core 也有一些可用的框架,例如:
让我们选择最小起订量。为了使用它,将Moq
NuGet 包添加到您的项目中。然后,当需要模拟给定类型的功能时,可以创建该类型的模拟并设置其行为,如图所示:
//create the mock
var mock = new Mock<ILogger<HomeController>>();
//setup an implementation for the Log method
mock.Setup(x => x.Log(LogLevel.Critical, new EventId(), "", null, null));
//get the mock
ILogger<HomeController> logger = mock.Object;
//call the mocked method with some parameters
logger.Log(LogLevel.Critical, new EventId(2), "Hello, Moq!", null, null);
通过传递一个表达式来设置方法,该表达式由具有适当参数类型的方法或属性调用组成,而不考虑其实际值。您可以将模拟对象作为服务或控制器的依赖项传递,运行测试,然后确保调用了模拟方法:
mock.Verify(x => x.Log(LogLevel.Critical, new EventId(), "", null, null));
也可以设置响应对象,例如,如果我们正在模拟HttpContext
:
var mock = new Mock<HttpContext>();
mock.Setup(x => x.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(new[] { new
Claim(ClaimTypes.Name, "username"), new Claim(ClaimTypes
.Role, "Admin") }, "Cookies")));
var context = mock.Object;
var user = context.User;
Assert.NotNull(user);
Assert.True(user.Identity.IsAuthenticated);
Assert.True(user.HasClaim(ClaimTypes.Name, "username"));
在这里,您可以看到,我们正在为调用User
属性提供返回值,并且我们正在返回一个预构建的ClaimsPrincipal
对象,其中包含所有的铃铛和哨子。
当然,Moq 还有很多,但我认为这应该足以让你开始。
断言
如果抛出异常,单元测试将失败。因此,您可以推出自己的异常抛出代码,也可以依赖单元测试框架提供的断言方法之一,该方法实际上抛出异常本身;它们都提供了类似的方法。
For more complex scenarios, it may be useful to use an assertion library. FluentAssertions
is one such library that happens to work nicely with .NET Core. Get it from NuGet as FluentAssertions
and from GitHub at https://github.com/fluentassertions/fluentassertions.
使用代码,您可以有如下断言:
int x = GetResult();
x
.Should()
.BeGreaterOrEqualTo(100)
.And
.BeLessOrEqualTo(1000)
.And
.NotBe(150);
如您所见,您可以组合许多与同一对象类型相关的表达式;数值有比较,字符串有匹配,等等。您还可以加入属性更改检测:
svc.ShouldRaisePropertyChangeFor(x => x.SomeProperty);
此外,还可以添加执行时间:
svc
.ExecutionTimeOf(s => s.LengthyMethod())
.ShouldNotExceed(500.Milliseconds());
There's a lot more to it, so I advise you to have a look at the documentation, available at http://fluentassertions.com.
接下来是用户界面。
用户界面
到目前为止,我们看到的单元测试是用于测试 API 的,比如业务方法和逻辑。但是,也可以测试用户界面。让我们看看使用硒对有何帮助。Selenium 是一个可移植的 web 应用软件测试框架,其.NET 端口为Selenium.WebDriver
。除此之外,我们还需要以下方面:
Selenium.Chrome.WebDriver
:对于铬Selenium.Firefox.WebDriver
:针对 FirefoxSelenium.WebDriver.MicrosoftWebDriver
:用于 Internet Explorer 和 Edge
我们首先创建一个驱动程序:
using (var driver = (IWebDriver) new ChromeDriver(Environment.CurrentDirectory))
{
//...
}
注意Environment.CurrentDirectory
参数;这指定了驱动程序在 Firefox 中可以找到chromedriver.exe
文件-geckodriver.exe
的路径,在 Internet Explorer/Edge 中可以找到MicrosoftWebDriver.exe
(当然是 Windows!)。这些可执行文件由 NuGet 包自动添加。此外,如果不处理驱动程序,单元测试完成后窗口将保持打开状态。您也可以随时拨打Quit
关闭浏览器。
现在,我们可以导航到任何页面:
driver
.Navigate()
.GoToUrl("http://www.google.com");
我们可以从其名称中找到一个元素:
var elm = driver.FindElement(By.Name("q"));
除了名称,我们还可以通过以下参数进行搜索:
- ID:
By.Id
- CSS 类:
By.ClassName
- CSS 选择器:
By.CssSelector
-
标签名称:
By.TagName
-
链接文本:
By.LinkText
- 部分链接文本:
By.PartialLinkText
- XPath:
By.XPath
找到元素后,我们可以访问其属性:
var attr = elm.GetAttribute("class");
var css = elm.GetCssValue("display");
var prop = elm.GetProperty("enabled");
我们还可以发送击键:
elm.SendKeys("asp.net");
我们也可以单击以下按钮,而不是按键:
var btn = driver.FindElement(By.Name("btnK"));
btn.Click();
正如我们所知,页面加载可能需要一些时间,因此我们可以配置默认时间以等待加载,可能在我们执行GoToUrl
之前:
var timeouts = driver.Manage().Timeouts();
timeouts.ImplicitWait = TimeSpan.FromSeconds(1);
timeouts.PageLoad = TimeSpan.FromSeconds(5);
ImplicitWait
是硒在寻找元素之前等待的时间;我相信你能猜出PageLoad
的作用。
如果我们需要等待一段时间,例如直到 AJAX 请求完成,我们可以这样做:
var waitForElement = new WebDriverWait(driver, TimeSpan.FromSeconds(5));
var logo = waitForElement.Until(ExpectedConditions.ElementIsVisible(By.Id("hplogo")));
传递给ExpectedConditions
的条件可以是以下条件之一:
AlertIsPresent
AlertState
ElementExists
ElementIsVisible
ElementSelectionStateToBe
-
ElementToBeClickable
-
ElementToBeSelected
FrameToBeAvailableAndSwitchToIt
InvisibilityOfElementLocated
InvisibilityOfElementWithText
PresenceOfAllElementsLocatedBy
StalenessOf
TextToBePresentInElement
TextToBePresentInElementLocated
TextToBePresentInElementValue
TitleContains
TitleIs
UrlContains
UrlMatches
UrlToBe
VisibilityOfAllElementsLocatedBy
正如你所看到的,你可以使用大量的条件。如果在计时器到期前不满足该条件,Until
返回的值为null
。
希望有了它,您能够编写单元测试来检查站点的用户界面。当然,它们需要指向一个活动环境,因此在这种情况下,测试不会是自包含的。当我们讨论集成测试时,我们将看到如何克服这一问题。
For more information about Selenium, please refer to https://selenium.dev.
这就是我们将要讨论的关于用户界面的内容。现在让我们看看如何从命令行运行测试。
使用命令行
dotnet
命令行工具是.NET Core 开发的瑞士军刀,因此,它完全支持运行单元测试。如果您在有单元测试的项目文件夹中,只需运行dotnet test
即可:
C:\Users\Projects\MyApp\UnitTests>dotnet test
Build started, please wait...
Build completed.
Test run for C:\Users\Projects\MyApp\UnitTests\bin\Debug\netcoreapp3.0\UnitTests.dll(.NETCoreApp,Version=v3.0)
Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
[xUnit.net 00:00:00.3769248] Discovering: UnitTests
[xUnit.net 00:00:00.4364853] Discovered: UnitTests
[xUnit.net 00:00:00.4720996] Starting: UnitTests
[xUnit.net 00:00:00.5778764] Finished: UnitTests
Total tests: 10\. Passed: 10\. Failed: 0\. Skipped: 0.
Test Run Successful.
Test execution time: 1,0031 Seconds
Since the project is set up to use xUnit (the xunit.runner.visualstudio
package), dotnet
is happy to use it automatically.
如果您希望查看已定义的所有测试,请改为运行dotnet test --list-tests
:
Test run for C:\Users\Projects\MyApp\UnitTests\bin\Debug\netcoreapp3.0\UnitTests.dll(.NETCoreApp,Version=v3.0)
Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation. All rights reserved.
The following Tests are available:
CanExecuteIndex
Add(1, 2, 3)
Add(0, 10, 10)
现在让我们看看单元测试的一些局限性。
单元测试的局限性
尽管单元测试很有用,但请记住它们基本上用于回归测试,并且它们有一些局限性:
- 它们通常不涉及用户界面,尽管存在一些可以这样做的框架(在撰写本文时,没有针对.NETCore 的框架)。
- 无法测试某些 ASP.NET 功能,例如筛选器或视图。
- 外部系统是模拟的,因此您只能有限地查看系统的一小部分。
我们之前看到了如何在用户界面上执行测试。在下一节中,我们将看到如何克服最后两个限制。
进行集成测试
在这里,我们将不仅仅把集成测试看作是一种执行带有一些输入参数的测试方法并断言结果或是否抛出异常的测试,还将其看作是执行真实代码的测试。集成测试将不同的代码模块测试在一起,而不仅仅是单个模块。
作为 ASP.NET Core 的一部分,微软已经推出了Microsoft.AspNetCore.Mvc.Testing
NuGet 软件包。本质上,它允许我们托管一个 web 应用,这样我们就可以像在现实服务器中一样在它上执行测试,当然,这就消除了性能和可伸缩性问题。
在单元测试项目中,创建如下类(同样,我们使用的是 xUnit):
public class IntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public IntegrationTests(WebApplicationFactory<Startup> factory)
{
this._factory = factory;
}
[Theory]
[InlineData("/")]
public async Task CanCallHome(string url)
{
//Arrange
var client = this._factory.CreateClient();
//Act
var response = await client.GetAsync(url);
//Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Welcome", content);
}
}
那么,我们这里有什么?如果您还记得在xUnit部分中,我们有一个单元测试类,在该类中,我们使用用于适当应用的相同Startup
类注入WebApplicationFactory
。然后,我们发出一个 URL 请求,该 URL 作为内联数据注入。在我们得到响应后,我们验证它的状态代码(EnsureSuccessStatusCode
检查我们没有4xx
或5xx
,并且我们实际上查看了返回的内容。请注意,在这里,我们不使用IActionResults
或类似工具,而是使用 HTTP 响应。因为WebApplicationFactory
使用约定,所以它知道从何处加载配置文件和程序集。
这种方法的优点是,我们在一个类似 web 的场景中真正测试我们的控制器(及其所有服务),这意味着将运行过滤器、检查身份验证、像往常一样加载配置等等。这与单元测试配合得很好,从本例中可以看出。
Notice that in this example, the unit test method is asynchronous; this is supported by xUnit and the other unit test frameworks.
您会注意到,我们将响应读取为字符串(response.Content.ReadAsStringAsync
。这意味着我们得到的响应是纯 HTML,这可能是我们想要的,也可能不是。我们可以使用像AngleSharp这样的库来解析这个 HTML 并从中构建 DOM。然后,您可以使用类似于浏览器上的方法进行查询。AngleSharp 作为 NuGet 套装提供。
最后一句话,您可能需要调整WebApplicationFactory
类以添加一些额外的配置或行为。这只是从它继承并重写其虚拟方法的问题。例如,假设要禁用在依赖项注入框架上注册的所有后台服务:
class MyCustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override IHostBuilder CreateHostBuilder()
{
return base
.CreateHostBuilder()
.ConfigureServices(services =>
{
services.RemoveAll<IHostedService>();
});
}
}
如您所见,在运行基类的CreateHostBuilder
参数后,我们将删除IHostBuilder
的所有注册实例。我们还可以将 start-up 类更改为其他类,或者执行任何其他类型的定制。只是在IClassFixture<T>
上指定这个类而不是WebApplicationFactory<Startup>
的问题。
我们已经了解了如何使用单元测试框架来实际执行集成测试。当然,请注意,以自动化的方式执行此操作将导致您的测试运行更长时间,并可能产生副作用,这与单元测试的理念背道而驰。
总结
你绝对应该对你的应用进行单元测试。无论您是否严格遵循 TDD,它们都非常有用,特别是对于回归测试。大多数持续集成工具完全支持运行单元测试。只是不要试图掩盖一切;关注应用的关键部分,如果时间允许,然后继续其他部分。认为我们将在大多数项目中拥有 100%的覆盖率是不合理的,因此我们需要做出决策。模拟框架在这里起着至关重要的作用,因为它们允许我们很好地模拟第三方服务。
正如我们在这里看到的,自动化集成测试允许我们测试单元测试中不可用的特性,这些特性涵盖了我们需要的其他部分。
本章介绍了测试我们的应用的方法,可以是单独测试应用的一部分,也可以是整个系统。单元测试很有用,因为它们可以确保我们的应用仍然按照它应该的方式工作,即使我们正在对它进行更改。
在下一章中,我们将讨论使用 ASP.NET Core 进行客户端开发。
问题
因此,在本章结束时,您应该知道以下问题的答案:
- 什么是比较流行的单元测试框架?
- 嘲笑的好处是什么?
- 单元测试和集成测试之间的区别是什么?
- 什么是 TDD?
- 单元测试有哪些限制?
- 我们如何将数据自动传递给单元测试?
- 红绿重构是什么意思?
版权属于:月萌API www.moonapi.com,转载请注明出处