前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《ASP.NET Core 微服务实战》-- 读书笔记(第3章)

《ASP.NET Core 微服务实战》-- 读书笔记(第3章)

作者头像
郑子铭
发布2021-01-13 11:53:51
8160
发布2021-01-13 11:53:51
举报
文章被收录于专栏:DotNet NB && CloudNative

第 3 章 使用 ASP.NET Core 开发微服务

微服务定义

微服务是一个支持特定业务场景的独立部署单元。它借助语义化版本管理、定义良好的 API 与其他后端服务交互。它的天然特点就是严格遵守单一职责原则。

为什么要用 API 优先

所有团队都一致把公开、文档完备且语义化版本管理的 API 作为稳定的契约予以遵守,那么这种契约也能让各团队自主地掌握其发布节奏。遵循语义化版本规则能让团队在完善 API 的同时,不破坏已有消费方使用的 API。

作为微服务生态系统成功的基石,坚持好 API 优先的这些实践,远比开发服务所用的技术或代码更重要。

以测试优先的方式开发控制器

每一个单元测试方法都包含如下三个部分:

  • 安排(Arrange)完成准备测试的必要配置
  • 执行(Act)执行被测试的代码
  • 断言(Assert)验证测试条件并确定测试是否通过

测试项目: https://github.com/microservices-aspnetcore/teamservice

特别注意测试项目如何把其他项目引用进来,以及为什么不需要再次声明从主项目继承而来的依赖项。

StatlerWaldorfCorp.TeamService.Tests.csproj

代码语言:javascript
复制
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="../../src/StatlerWaldorfCorp.TeamService/StatlerWaldorfCorp.TeamService.csproj"/>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0-preview-20170210-02" />
    <PackageReference Include="xunit" Version="2.2.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
  </ItemGroup>

</Project>

首先创建 Team 模型类

Team.cs

代码语言:javascript
复制
using System;
using System.Collections.Generic;

namespace StatlerWaldorfCorp.TeamService.Models
{
    public class Team {

        public string Name { get; set; }
        public Guid ID { get; set; }
        public ICollection<Member> Members { get; set; }

        public Team()
        {
            this.Members = new List<Member>();
        }

        public Team(string name) : this()
        {
            this.Name = name;
        }

        public Team(string name, Guid id)  : this(name)
        {
            this.ID = id;
        }

        public override string ToString() {
            return this.Name;
        }
    }
}

每个团队都需要一系列成员对象

Member.cs

代码语言:javascript
复制
using System;

namespace StatlerWaldorfCorp.TeamService.Models
{
    public class Member {
        public Guid ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Member() {
        }

        public Member(Guid id) : this() {
            this.ID = id;
        }

        public Member(string firstName, string lastName, Guid id) : this(id) {
            this.FirstName = firstName;
            this.LastName = lastName;
        }

        public override string ToString() {
            return this.LastName;
        }
    }
}

创建第一个失败的测试

TeamsControllerTest.cs

代码语言:javascript
复制
using Xunit;
using System.Collections.Generic;
using StatlerWaldorfCorp.TeamService.Models;

namespace StatlerWaldorfCorp.TeamService
{
    public class TeamsControllerTest
    {
        TeamsController controller = new TeamsController();

        [Fact]
        public void QueryTeamListReturnsCorrectTeams()
        {
            List<Team> teams = new List<Team>(controller.GetAllTeams());
        }
    }
}

要查看测试运行失败的结果,请打开一个终端并运行 cd 浏览到对应目录,然后运行以下命令:

代码语言:javascript
复制
$ dotnet restore
$ dotnet test

因为被测试的控制器尚未创建,所以测试项目无法通过。

向主项目添加一个控制器:

TeamsController.cs

代码语言:javascript
复制
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using StatlerWaldorfCorp.TeamService.Models;

namespace StatlerWaldorfCorp.TeamService
{
    public class TeamsController
    {
        public TeamsController()
        {
            
        }

        [HttpGet]
        public IEnumerable<Team> GetAllTeams()
        {
            return Enumerable.Empty<Team>();
        }
    }
}

第一个测试通过后,我们需要添加一个新的、运行失败的断言,检查从响应里获取的团队数目是正确的,由于还没创建模拟对象,先随意选择一个数字。

代码语言:javascript
复制
List<Team> teams = new List<Team>(controller.GetAllTeams());
Assert.Equal(teams.Count, 2);

现在让我们在控制器里硬编码一些随机的逻辑,使测试通过。

只编写恰好能让测试通过的代码,这样的小迭代作为 TDD 规则的一部分,不光是一种 TDD 运作方式,更能直接提高对代码的信心级别,同时也能避免 API 逻辑膨胀。

更新后的 TeamsController 类,支持新的测试

代码语言:javascript
复制
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using StatlerWaldorfCorp.TeamService.Models;

namespace StatlerWaldorfCorp.TeamService
{
    public class TeamsController
    {
        public TeamsController()
        {
            
        }

        [HttpGet]
        public IEnumerable<Team> GetAllTeams()
        {
            return new Team[] { new Team("One"), new Team("Two") };
        }
    }
}

接下来关注添加团队方法。

代码语言:javascript
复制
[Fact]
public void CreateTeamAddsTeamToList()
{
    TeamsController controller = new TeamsController();
    var teams = (IEnumerable<Team>)(await controller.GetAllTeams() as ObjectResult).Value;
    List<Team> original = new List<Team>(teams);
    
    Team t = new Team("sample");
    var result = controller.CreateTeam(t);

    var newTeamsRaw = (IEnumerable<Team>)(controller.GetAllTeams() as ObjectResult).Value;
    
    List<Team> newTeams = new List<Team>(newTeamsRaw);
    Assert.Equal(newTeams.Count, original.Count+1);
    var sampleTeam = newTeams.FirstOrDefault( target => target.Name == "sample");
    Assert.NotNull(sampleTeam);
}

代码略粗糙,测试通过后可以重构测试以及被测试代码。

在真实世界的服务里,不应该在内存中存储数据,因为会违反云原生服务的无状态规则。

接下来创建一个接口表示仓储,并重构控制器来使用它。

ITeamRepository.cs

代码语言:javascript
复制
using System.Collections.Generic;

namespace StatlerWaldorfCorp.TeamService.Persistence
{
    public interface ITeamRepository {
        IEnumerable<Team> GetTeams();
        void AddTeam(Team team);
    }
}

在主项目中为这一仓储接口创建基于内存的实现

MemoryTeamRepository.cs

代码语言:javascript
复制
using System.Collections.Generic;

namespace StatlerWaldorfCorp.TeamService.Persistence
{
    public class MemoryTeamRepository :  ITeamRepository {
        protected static ICollection<Team> teams;

        public MemoryTeamRepository() {
            if(teams == null) {
                teams = new List<Team>();
            }
        }

        public MemoryTeamRepository(ICollection<Team> teams) {
            teams = teams;
        }

        public IEnumerable<Team> GetTeams() {
            return teams;
        }

        public void AddTeam(Team t)
        {
            teams.Add(t);
        }
    }
}

借助 ASP.NET Core 的 DI 系统,我们将通过 Startup 类把仓储添加为 DI 服务

代码语言:javascript
复制
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddScoped<ITeamRepository, MemoryTeamRepository>();
}

利用这种 DI 服务模型,现在我们可以在控制器里使用构造函数注入,而 ASP.NET Core 则会把仓储实例添加到所有依赖它的控制器里。

修改控制器,通过给构造函数添加一个简单参数就把它注入进来

代码语言:javascript
复制
public class TeamsController : Controller
{
    ITeamRepository repository;

    public TeamsController(ITeamRepository repo)
    {
        repository = repo;
    }
    
    ...
}

修改现有的控制器方法,将使用仓储,而不是返回硬编码数据

代码语言:javascript
复制
[HttpGet]
public async virtual Task<IActionResult> GetAllTeams()
{
    return this.Ok(repository.GetTeams());
}

可从 GitHub 的 master 分支找到测试集的完整代码

要立即看这些测试的效果,请先编译服务主项目,然后转到 test/StatlerWaldorfCorp.TeamService.Tests 目录,并运行下列命令:

代码语言:javascript
复制
$ dotnet restore
$ dotnet build
$ dotnet test
集成测试

集成测试最困难的部分之一经常位于启动 Web 宿主机制的实例时所需要的技术或代码上,我们在测试中需要借助 Web 宿主机制收发完整的 HTTP 消息。

庆幸的是,这一问题已由 Microsoft.AspNetCore.TestHost.TestServer类解决。

对不同场景进行测试

SimpleIntegrationTests.cs

代码语言:javascript
复制
using Xunit;
using System.Collections.Generic;
using StatlerWaldorfCorp.TeamService.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
using System;
using System.Net.Http;
using System.Linq;
using Newtonsoft.Json;
using System.Text;

namespace StatlerWaldorfCorp.TeamService.Tests.Integration
{
    public class SimpleIntegrationTests
    {
        private readonly TestServer testServer;
        private readonly HttpClient testClient;
        
        private readonly Team teamZombie;

        public SimpleIntegrationTests()
        {
            testServer = new TestServer(new WebHostBuilder()
                    .UseStartup<Startup>());
            testClient = testServer.CreateClient();

            teamZombie = new Team() {
                ID = Guid.NewGuid(),
                Name = "Zombie"
            };
        }

        [Fact]
        public async void TestTeamPostAndGet()
        {
            StringContent stringContent = new StringContent(
                JsonConvert.SerializeObject(teamZombie),
                UnicodeEncoding.UTF8,
                "application/json");

            // Act
            HttpResponseMessage postResponse = await testClient.PostAsync(
                "/teams",
                stringContent);
            postResponse.EnsureSuccessStatusCode();

            var getResponse = await testClient.GetAsync("/teams");
            getResponse.EnsureSuccessStatusCode();

            string raw = await getResponse.Content.ReadAsStringAsync();
            List<Team> teams = JsonConvert.DeserializeObject<List<Team>>(raw);
            Assert.Equal(1, teams.Count());
            Assert.Equal("Zombie", teams[0].Name);
            Assert.Equal(teamZombie.ID, teams[0].ID);
        }
    }
}
运行团队服务的 Docker 镜像
代码语言:javascript
复制
$ docker run -p 8080:8080 dotnetcoreseservices/teamservice

端口映射之后,就可以用 http://localhost:8080 作为服务的主机名

下面的 curl 命令会向服务的 /teams 资源发送一个 POST 请求

代码语言:javascript
复制
$ curl -H "Content-Type:application/json" \ -X POST -d \ '{"id":"e52baa63-d511-417e-9e54-7aab04286281", \ "name":"Team Zombie"}' \ http://localhost:8080/teams

它返回了一个包含了新创建团队的 JSON 正文

代码语言:javascript
复制
{"name":"Team Zombie","id":"e52baa63-d511-417e-9e54-7aab04286281","members":[]}

注意上面片段的响应部分,members 属性是一个空集合。

为确定服务在多个请求之间能够维持状态(即使目前只是基于内存列表实现),我们可以使用下面的 curl 命令

代码语言:javascript
复制
$ curl http://localhost:8080/teams
[{"name":"Team Zombie","id":"e52baa63-d511-417e-9e54-7aab04286281","members":[]}]

至此,我们已经拥有了一个功能完备的团队服务,每次 Git 提交都将触发自动化测试,将自动部署到 docker hub,并未云计算环境的调度做好准备。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第 3 章 使用 ASP.NET Core 开发微服务
    • 微服务定义
      • 为什么要用 API 优先
        • 以测试优先的方式开发控制器
          • 集成测试
            • 运行团队服务的 Docker 镜像
            相关产品与服务
            专用宿主机
            专用宿主机(CVM Dedicated Host,CDH)提供用户独享的物理服务器资源,满足您资源独享、资源物理隔离、安全、合规需求。专用宿主机搭载了腾讯云虚拟化系统,购买之后,您可在其上灵活创建、管理多个自定义规格的云服务器实例,自主规划物理资源的使用。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档