zoukankan      html  css  js  c++  java
  • 《ASP.NET Core 微服务实战》-- 读书笔记(第3章)

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

    微服务定义

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

    为什么要用 API 优先

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

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

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

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

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

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

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

    StatlerWaldorfCorp.TeamService.Tests.csproj

    <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

    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

    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

    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 浏览到对应目录,然后运行以下命令:

    $ dotnet restore
    $ dotnet test
    

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

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

    TeamsController.cs

    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>();
    		}
    	}
    }
    

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

    List<Team> teams = new List<Team>(controller.GetAllTeams());
    Assert.Equal(teams.Count, 2);
    

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

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

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

    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") };
    		}
    	}
    }
    

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

    [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

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

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

    MemoryTeamRepository.cs

    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 服务

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddScoped<ITeamRepository, MemoryTeamRepository>();
    }
    

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

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

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

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

    [HttpGet]
    public async virtual Task<IActionResult> GetAllTeams()
    {
    	return this.Ok(repository.GetTeams());
    }
    

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

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

    $ dotnet restore
    $ dotnet build
    $ dotnet test
    

    集成测试

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

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

    对不同场景进行测试

    SimpleIntegrationTests.cs

    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 镜像

    $ docker run -p 8080:8080 dotnetcoreseservices/teamservice
    

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

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

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

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

    {"name":"Team Zombie","id":"e52baa63-d511-417e-9e54-7aab04286281","members":[]}
    

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

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

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

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

    知识共享许可协议

    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

    欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

    如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。

  • 相关阅读:
    Linux文件和目录的属性及权限
    chkconfig原理
    Linux启动过程
    正则表达式(grep,awk,sed)和通配符
    Linux系统目录结构:目录层次标准、常用目录和文件
    Linux系统目录结构
    虚拟机快照和克隆
    Linux系统的基础优化
    Linux系统应用管理:增加普通用户(密码管理等)
    [译]java9新特性:在接口中用pirvate方法让default(java8接口特性)更简练
  • 原文地址:https://www.cnblogs.com/MingsonZheng/p/12258148.html
Copyright © 2011-2022 走看看