3. 使用xUnit进行单元测试

实现.NET Core时,xUnit可用于创建单元测试,.NET Core团队使用了该产品。xUnit是一个开源实现方案,创建NUnit 2.0的开发人员创建了它。现在,.NET Core命令行界面支持MSTest和xUnit。

提示:

xUnit的文档可参阅https://xunit.github.io/

Visual Studio测试环境支持其他测试框架。测试适配器,如NUnit、xUnit、Boost(用于C++)、Chutzpah(用于JavaScript)和Jasmine(用于JavaScript)可通过扩展和更新来使用;这些测试适配器与Visual Studio Test Explorer集成。

xUnit是.NET Core中一个杰出的测试框架,也由微软的.NET Core和ASP .NET Core开源代码使用,所以xUnit是本节的重点。

1. 使用xUnit和.NET Core

使用.NET Core应用程序,可以创建xUnit测试,其方式与MSTest测试类似。从命令行,可以使用:

> dotnet new xunit

创建xUnit测试项目。在Visual Studio 2017中,可以选择项目类型xUnit Test Project(.NET Core)。

在示例项目中,测试与以前相同的.NET 标准库UnitTestingSamples。这个库包含之前所示的测试的类型:DeepThought和StringSample。测试项目的名称是UnitTestingSamples.xUnit.Tests。

这个项目需要引用xunit(对于单元测试,是xunit.runner.visualstudio[在Visual Studio中运行测试])和UnitTestingSamples项目(应测试的代码)。为了与.NET Core命令行集成,添加dotnet-xunit的DotNetCliToolReference



  
    netcoreapp3.1

    false
  

  
    
    
    
    
  

  
    
  

2. 创建Fact属性

创建测试的方式非常类似于之前的方法。在MSTest中,需要给测试类型添加特性注释([TestClass])。但在xUnit中是不必要的。因为会在所有的公共类中搜索测试方法。在xUnit和MSTest中测试方法TheAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything的差异只是测试方法带Fact特性注释和不同的Assert.Equal方法:

    public class DeepThoughtTest
    {
        [Fact]
        public void ResultOfTheAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything()
        {
            int expected = 42;
            var dt = new DeepThought();
            int actual = dt.TheAnswerOfTheUltimateQuestionOfLifeUniverseAndEverything();
            Assert.Equal(expected,actual);
        }
    }

现在使用的Assert类在xUnit名称空间中定义。与MSTest的Assert方法相比,这个类定义了更多的方法,用于验证。例如,不是添加一个特性来指定预期的异常,而是使用Assert.Throws方法,允许在一个测试方法中多次检查异常:

    public class StringSampleTest
    {
        [Fact]
        public void GetStringDemoExceptions()
        {
            var sample = new StringSample(string.Empty);
            Assert.Throws(()=>sample.GetStringDemo(null,"a"));
            Assert.Throws(() => sample.GetStringDemo("a",null));
            Assert.Throws(()=>sample.GetStringDemo(string.Empty,"a"));
        }
    }

3. 创建Theory特性

xUnit为不需要参数的测试方法定义Fact特性。使用xUnit还可以调用需要参数的单元测试方法:使用Theory特性提供数据,添加一个派生于Data的特性。这样就可以通过一个方法定义多个单元测试了。

在下面的代码片段中,Theory特性应用于GetStringDemoInlineData单元测试方法。StringSample.GetStringDemo方法定义了取决于输入数据的不同路径。如果第二个参数传递的字符串不包含在第一个参数中,就到达第一条路径。如若第二个字符串包含在第一个字符串的前5个字符串中,就到达第二条路径。第三条路径是用else子句到达的。要到达所有不同的路径,3个InlineData特性应用于测试方法。每个特性都定义了4个参数,它们以相同的顺序直接发送到单元测试方法的调用中。特性还定义了被测试方法应该返回的值:

        [Theory]
        [InlineData("","longer string","nger","removed nger from longer string: lo string")]
        [InlineData("init","longer string","string","INIT")]
        public void GetStringDemoInlineData(string init,string a,string b,string expected)
        {
            var sample = new StringSample(init);
            string actual = sample.GetStringDemo(a,b);
            Assert.Equal(expected,actual);
        }

特性InlineData派生于Data特性。除了通过特性直接把值提供给测试方法之外,值也可以来自于属性、方法或类。以下例子定义了一个静态方法,它用IEnumeralbe对象返回相同的值:

        public static IEnumerable GetStringSampleData()
            => new []
            {
                new object[]{ "","a","b","b not found in a"},
                new object[]{"","longer string","nger","removed nger from longer string: lo string"},
                new object[]{ "init","longer string","string","INIT"}
            };

单元测试方法现在用MemberData特性改变了。这个特性允许使用返回IEnumerable的静态属性或方法,填写单元测试的参数:

        [Theory]
        [MemberData(nameof(GetStringSampleData))]
        public void GetStringDemoMemberData(string init, string a, string b, string expected)
        {
            var sample = new StringSample(init);
            var actual = sample.GetStringDemo(a, b);
            Assert.Equal(expected, actual);
        }

测试运行结果:

3. 使用xUnit进行单元测试_第1张图片

4. 使用Mocking库

下面是一个更复杂的例子:在MVVM应用程序中,为客户端服务库创建一个单元测试。本章的示例代码仅包含该应用程序使用的一个库。这个服务使用依赖注入功能,注入接口IBooksRepository定义的存储库。用于测试AddOrUpdateBookAsync方法的单元测试不应该测试该库,而只测试方法中的功能。对于库,应执行另一个单元测试:下面的代码段显示了类的实现:

    public class BooksService : IBooksService
    {
        private ObservableCollection _books = new ObservableCollection();
        public IEnumerable Books => _books;
        private IBookRepository _booksRepository;
        public BooksService(IBookRepository booksRepository)
        {
            _booksRepository = booksRepository;
        }

        public async Task AddOrUpdateBookAsync(Book book)
        {
            if (book is null)
            {
                throw new ArgumentNullException(nameof(book));
            }
            Book updated = null;
            if (book.BookId == 0)
            {
                updated = await _booksRepository.AddAsync(book);
                _books.Add(updated);
            }
            else
            {
                updated = await _booksRepository.UpdateAsync(book);
                if (updated is null)
                {
                    throw new InvalidOperationException();
                }
                Book old = _books.Where(b => b.BookId == updated.BookId).Single();
                int ix = _books.IndexOf(old);
                _books.RemoveAt(ix);
                _books.Insert(ix,updated);
            }
            return updated;
        }

        public Book GetBook(int bookId)
        {
            return _books.Where(b => b.BookId == bookId).SingleOrDefault();
        }

        public async Task LoadBooksAsync()
        {
            if (_books.Count > 0)
            {
                return;
            }
            IEnumerable books = await _booksRepository.GetItemAsync();
            //_books.Clear();
            foreach (var book in books)
            {
                _books.Add(book);
            }
        }
    }

因为AddOrUpdateBookAsync的单元测试不应该测试用于IBooksRepository的存储库,所以需要实现一个用于测试的存储库。为了简单起见,可以使用一个模拟库自动填充空白。一个常用的模拟库是Moq。对于单元测试项目,添加NuGet包Moq。

注意:

除了使用Moq框架之外,还可以用示例数据实现一个内存中的存储库。在用户界面的设计过程中,可以这么做来处理应用程序的示例数据

使用xUnit时,每次运行测试都会创建测试类的一个新实例。如果多个测试需要相同的功能,就可以把这个功能移动到构造函数中。如果每次运行测试后需要释放资源,就可以实现IDisposable接口。

在BooksServiceTest类的构造函数中,实例化一个Mock对象,传递泛型参数IBooksRepository。Mock构造函数创建接口的实现代码。因为需要从存储库中得到一些非空结果来创建有用的测试,所以Setup方法定义可以传递的参数,ReturnAsync方法定义了方法存根返回的结果。使用Mock类的Object属性访问模拟对象,并传递它,以创建BooksService类的实例。有了这些设置,就可以实现单元测试:

    public class BooksServiceTest : IDisposable
    {
        public void Dispose()
        {
        }
        private const string TestTitle = "Test Title";
        private const string UpdatedTestTitle = "Updated Test Title";
        private const string APublisher = "A Publisher";
        private BooksService _bookService;
        private Book _newBook = new Book
        {
            BookId = 0,
            Title = TestTitle,
            Publisher = APublisher
        };
        private Book _expectedBook = new Book
        {
            BookId = 1,
            Title = TestTitle,
            Publisher = APublisher
        };
        private Book _notInRepositoryBook = new Book
        {
            BookId = 42,
            Title = TestTitle,
            Publisher = APublisher
        };
        private Book _updatedBook = new Book
        {
            BookId = 1,
            Title = UpdatedTestTitle,
            Publisher = APublisher
        };
        public BooksServiceTest()
        {
            var mock = new Mock();
            mock.Setup(repository =>
            repository.AddAsync(_newBook)).ReturnsAsync(_expectedBook);
            mock.Setup(repository =>
            repository.UpdateAsync(_notInRepositoryBook)).ReturnsAsync(null as Book);
            mock.Setup(repository=>
            repository.UpdateAsync(_updatedBook)).ReturnsAsync(_updatedBook);
            _bookService = new BooksService(mock.Object);
        }
        [Fact]
        public void Test1()
        {

        }
    }

实现的第一个单元测试AddOrUpdateBookAsync_ThrowFouNull证明,如果把null传递给AddOrUpdateBookAsync方法,就会抛出ArgumentNullException异常。该实现代码只需要在构造函数中实例化成员变量_booksService,而不需要模拟设置。这个代码示例还说明,单元测试方法可以实现为返回Task的异步方法:

        public async Task AddOrUpdateBookAsync_ThrowNullException()
        {
            //arrange
            Book nullBook = null;
            //act an assert
            await Assert.ThrowsAsync(() =>
            _bookService.AddOrUpdateBookAsync(nullBook));
        }

单元测试方法AddOrUpdateBookAsync_AddedBookReturnsFromRepository给服务添加了一本新书(变量_newBook),并期望返回_expectedBook对象。在AddOrUpdateBookAsync方法的实现代码中,调用了IBooksRepository的AddAsync方法,因此,可以应用以前给这个方法定义的模拟设置。这个方法的结果应是,返回的Book等于_expectedBook,_expectedBook也需要添加到BooksService的图书集合中:

        public async Task AddOrUpdateBookAsync_AddedBookReturnsFromRepository()
        {
            //arrange in constructory
            //act
            Book actualAdded = await _bookService.AddOrUpdateBookAsync(_newBook);
            //assert
            Assert.Equal(_expectedBook, actualAdded);
            Assert.Contains(_expectedBook,_bookService.Books);
        }

AddOrUpdateBookAsync_UpdateNotExistingBookThrows单元测试证明,尝试更新服务中不存在的图书,应抛出InvalidOperationException异常:

        public async Task AddOrUpdateBookAsync_UpdateNotExistingBookThrow()
        {
            //arrange in constructor
            //act and assert
            await Assert.ThrowsAsync(()=>
            _bookService.AddOrUpdateBookAsync(_notInRepositoryBook));
        }

更新图书的常见情形用单元测试AddOrUpdateBookAsync_UpdateBook来处理。这里需要做额外的准备,在更新前,先把图书添加到服务中:

        [Fact]
        public async Task AddOrUpdateBookAsync_UpdateBook()
        {
            //arrange
            await _bookService.AddOrUpdateBookAsync(_newBook);
            //act
            Book updateBook = await _bookService.AddOrUpdateBookAsync(_updatedBook);
            //assert
            Assert.Equal(_updatedBook, updateBook);
            Assert.Contains(_updatedBook,_bookService.Books);
        }

当使用MVVM模式与基于XAML的应用程序,以及使用MVC模式和基于Web的应用程序时,会降低用户界面的复杂性,减少复杂UI测试的需求。然而,仍有一些场景应该用于UI测试,例如,浏览页面、拖拽元素等。此时应使用Visual Studio的UI测试功能。

你可能感兴趣的:(测试,C#)