《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) 。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM