2024年4月

引言

上一章节介绍了 TDD 的三大法则,今天我们讲一下在单元测试中模拟对象的使用。

Fake

Fake
-
Fake
是一个通用术语,可用于描述
stub

mock
对象。 它是
stub
还是
mock
取决于使用它的上下文。 也就是说,
Fake
可以是
stub

mock

Mock
-
Mock
对象是系统中的
fake
对象,用于确定单元测试是否通过。
Mock
起初为
Fake
,直到对其断言。

Stub
-
Stub
是系统中现有依赖项的可控制替代项。 通过使用
Stub
,可以在无需使用依赖项的情况下直接测试代码。

参考
单元测试最佳做法
让我们使用相同的术语

区别点:

  1. Stub

    • 用于提供可控制的替代行为,通常是在测试中模拟依赖项的简单行为。
    • 主要用于提供固定的返回值或行为,以便测试代码的特定路径。
    • 不涉及对方法调用的验证,只是提供一个虚拟的实现。
  2. Mock

    • 用于验证方法的调用和行为,以确保代码按预期工作。
    • 主要用于确认特定方法是否被调用,以及被调用时的参数和次数。
    • 可以设置期望的调用顺序、参数和返回值,并在测试结束时验证这些调用。

总结:

  • Stub
    更侧重于提供一个简单的替代品,帮助测试代码路径,而不涉及行为验证。
  • Mock
    则更侧重于验证代码的行为和调用,以确保代码按预期执行。

在某些情况下两者可能看起来相似,但在测试的目的和用途上还是存在一些区别。在编写单元测试时,根据测试场景和需求选择合适的
stub

mock
对象可以帮助提高测试的准确性和可靠性。

创建实战项目

创建一个
WebApi

Controller
项目,和一个
EFCore
仓储类库作为我们后续章节的演示项目

dotNetParadise-Xunit
│
├── src
│   ├── Sample.Api
│   └── Sample.Repository

Sample.Repository
是一个简单
EFCore
的仓储模式实现,
Sample.Api
对外提供
RestFul

Api
接口

Sample.Repository 实现

  • 第一步
    Sample.Repository
    类库安装
    Nuget
PM> NuGet\Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 8.0.3
PM> Microsoft.EntityFrameworkCore.Relational -Version 8.0.3
  • 创建实体
    Staff
public class Staff
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public int? Age { get; set; }
    public List<string>? Addresses { get; set; }

    public DateTimeOffset? Created { get; set; }
}
  • 创建
    SampleDbContext
    数据库上下文
public class SampleDbContext(DbContextOptions<SampleDbContext> options) : DbContext(options)
{
    public DbSet<Staff> Staff { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}
  • 定义仓储接口和实现
public interface IStaffRepository
{
    /// <summary>
    /// 获取 Staff 实体的 DbSet
    /// </summary>
    DbSet<Staff> dbSet { get; }

    /// <summary>
    /// 添加新的 Staff 实体
    /// </summary>
    /// <param name="staff"></param>
    Task AddStaffAsync(Staff staff, CancellationToken cancellationToken = default);

    /// <summary>
    /// 根据 Id 删除 Staff 实体
    /// </summary>
    /// <param name="id"></param>
     Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default);

    /// <summary>
    /// 更新 Staff 实体
    /// </summary>
    /// <param name="staff"></param>
    Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken = default);

    /// <summary>
    /// 根据 Id 获取单个 Staff 实体
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken = default);

    /// <summary>
    /// 获取所有 Staff 实体
    /// </summary>
    /// <returns></returns>
    Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken = default);

    /// <summary>
    /// 批量更新 Staff 实体
    /// </summary>
    /// <param name="staffList"></param>
    Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken = default);

}
  • 仓储实现
public class StaffRepository : IStaffRepository
{
    private readonly SampleDbContext _dbContext;
    public DbSet<Staff> dbSet => _dbContext.Set<Staff>();
    public StaffRepository(SampleDbContext dbContext)
    {
        dbContext.Database.EnsureCreated();
        _dbContext = dbContext;
    }
    public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken = default)
    {
        await dbSet.AddAsync(staff, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
    }

    public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
    {
        //await dbSet.AsQueryable().Where(_ => _.Id == id).ExecuteDeleteAsync(cancellationToken);
        var staff = await GetStaffByIdAsync(id, cancellationToken);
        if (staff is not null)
        {
            dbSet.Remove(staff);
            await _dbContext.SaveChangesAsync(cancellationToken);
        }
    }

    public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken = default)
    {
        dbSet.Update(staff);
        _dbContext.Entry(staff).State = EntityState.Modified;
        await _dbContext.SaveChangesAsync(cancellationToken);
    }

    public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await dbSet.AsQueryable().Where(_ => _.Id == id).FirstOrDefaultAsync(cancellationToken);
    }

    public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken = default)
    {
        return await dbSet.ToListAsync(cancellationToken);
    }

    public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken = default)
    {
        await dbSet.AddRangeAsync(staffList, cancellationToken);
        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}
  • 依赖注入
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddEFCoreInMemoryAndRepository(this IServiceCollection services)
    {
        services.AddScoped<IStaffRepository, StaffRepository>();
        services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("sample").EnableSensitiveDataLogging(), ServiceLifetime.Scoped);
        return services;
    }
}

到目前为止 仓储层的简单实现已经完成了,接下来完成
WebApi

Sample.Api


Sample.Api
添加项目引用
Sample.Repository

program
依赖注入

builder.Services.AddEFCoreInMemoryAndRepository();
  • 定义
    Controller
[Route("api/[controller]")]
[ApiController]
public class StaffController(IStaffRepository staffRepository) : ControllerBase
{
    private readonly IStaffRepository _staffRepository = staffRepository;

    [HttpPost]
    public async Task<IResult> AddStaff([FromBody] Staff staff, CancellationToken cancellationToken = default)
    {
        await _staffRepository.AddStaffAsync(staff, cancellationToken);
        return TypedResults.NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IResult> DeleteStaff(int id, CancellationToken cancellationToken = default)
    {
        await _staffRepository.DeleteStaffAsync(id);
        return TypedResults.NoContent();
    }

    [HttpPut("{id}")]
    public async Task<Results<BadRequest<string>, NoContent, NotFound>> UpdateStaff(int id, [FromBody] Staff staff, CancellationToken cancellationToken = default)
    {
        if (id != staff.Id)
        {
            return TypedResults.BadRequest("Staff ID mismatch");
        }
        var originStaff = await _staffRepository.GetStaffByIdAsync(id, cancellationToken);
        if (originStaff is null) return TypedResults.NotFound();
        originStaff.Update(staff);
        await _staffRepository.UpdateStaffAsync(originStaff, cancellationToken);
        return TypedResults.NoContent();
    }

    [HttpGet("{id}")]
    public async Task<Results<Ok<Staff>, NotFound>> GetStaffById(int id, CancellationToken cancellationToken = default)
    {
        var staff = await _staffRepository.GetStaffByIdAsync(id, cancellationToken);
        if (staff == null)
        {
            return TypedResults.NotFound();
        }
        return TypedResults.Ok(staff);
    }


    [HttpGet]
    public async Task<IResult> GetAllStaff(CancellationToken cancellationToken = default)
    {
        var staffList = await _staffRepository.GetAllStaffAsync(cancellationToken);
        return TypedResults.Ok(staffList);
    }


    [HttpPost("BatchAdd")]
    public async Task<IResult> BatchAddStaff([FromBody] List<Staff> staffList, CancellationToken cancellationToken = default)
    {
        await _staffRepository.BatchAddStaffAsync(staffList, cancellationToken);
        return TypedResults.NoContent();
    }

}

F5
项目跑一下

image

到这儿我们的项目已经创建完成了本系列后面的章节基本上都会以这个项目为基础展开拓展

控制器的单元测试

[
单元测试
涉及通过基础结构和依赖项单独测试应用的一部分。 单元测试控制器逻辑时,仅测试单个操作的内容,不测试其依赖项或框架自身的行为。

本章节主要以控制器的单元测试来带大家了解一下
Stup

Moq
的核心区别。

创建一个新的测试项目,然后添加
Sample.Api
的项目引用

image

Stub
实战

Stub
是系统中现有依赖项的可控制替代项。通过使用
Stub
,可以在测试代码时不需要使用真实依赖项。通常情况下,存根最初被视为
Fake

下面对
StaffController
利用
Stub
进行单元测试,

  • 创建一个
    Stub
    实现
    IStaffRepository
    接口,以模拟对数据库或其他数据源的访问操作。
  • 在单元测试中使用这个
    Stub
    替代
    IStaffRepository
    的实际实现,以便在不依赖真实数据源的情况下测试
    StaffController
    中的方法。

我们在
dotNetParadise.FakeTest
测试项目上新建一个
IStaffRepository
的实现,名字可以叫
StubStaffRepository

public class StubStaffRepository : IStaffRepository
{
    public DbSet<Staff> dbSet => default!;

    public async Task AddStaffAsync(Staff staff, CancellationToken cancellationToken)
    {
        // 模拟添加员工操作
        await Task.CompletedTask;
    }

    public async Task DeleteStaffAsync(int id)
    {
        // 模拟删除员工操作
        await Task.CompletedTask;
    }

    public async Task UpdateStaffAsync(Staff staff, CancellationToken cancellationToken)
    {
        // 模拟更新员工操作
        await Task.CompletedTask;
    }

    public async Task<Staff?> GetStaffByIdAsync(int id, CancellationToken cancellationToken)
    {
        // 模拟根据 ID 获取员工操作
        return await Task.FromResult(new Staff { Id = id, Name = "Mock Staff" });
    }

    public async Task<List<Staff>> GetAllStaffAsync(CancellationToken cancellationToken)
    {
        // 模拟获取所有员工操作
        return await Task.FromResult(new List<Staff> { new Staff { Id = 1, Name = "Mock Staff 1" }, new Staff { Id = 2, Name = "Mock Staff 2" } });
    }

    public async Task BatchAddStaffAsync(List<Staff> staffList, CancellationToken cancellationToken)
    {
        // 模拟批量添加员工操作
        await Task.CompletedTask;
    }

    public async Task DeleteStaffAsync(int id, CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask;
    }
}

我们新创建了一个仓储的实现来替换
StaffRepository
作为新的依赖

下一步在单元测试项目测试我们的
Controller
方法

public class TestStubStaffController
{

    [Fact]
    public async Task AddStaff_WhenCalled_ReturnNoContent()
    {
        //Arrange
        var staffController = new StaffController(new StubStaffRepository());
        var staff = new Staff()
        {
            Age = 10,
            Name = "Test",
            Email = "Test@163.com",
            Created = DateTimeOffset.Now,
        };
        //Act
        var result = await staffController.AddStaff(staff);

        //Assert
        Assert.IsType<Results<NoContent, ProblemHttpResult>>(result);
    }

    [Fact]
    public async Task GetStaffById_WhenCalled_ReturnOK()
    {
        //Arrange
        var staffController = new StaffController(new StubStaffRepository());
        var id = 1;
        //Act
        var result = await staffController.GetStaffById(id);

        //Assert
        Assert.IsType<Results<Ok<Staff>, NotFound>>(result);
        var okResult = (Ok<Staff>)result.Result;
        Assert.Equal(id, okResult.Value?.Id);
    }

      //先暂时省略后面测试方法....

}

image


Stub
来替代真实的依赖项,以便更好地控制测试环境和测试结果

Mock

在测试过程中,尤其是
TDD
的开发过程中,测试用例有限开发在这个时候,我们总是要去模拟对象的创建,这些对象可能是某个接口的实现也可能是具体的某个对象,这时候就必须去写接口的实现,这时候模拟对象
Mock
的用处就体现出来了,在社区中也有很多模拟对象的库如
Moq
,
FakeItEasy
等。

Moq
是一个简单、直观且强大的
.NET
模拟库,用于在单元测试中模拟对象和行为。通过
Moq
,您可以轻松地设置依赖项的行为,并验证代码的调用。

我们用上面的实例来演示一下
Moq
的核心用法

第一步
Nuget
包安装
Moq

PM> NuGet\Install-Package Moq -Version 4.20.70

您可以使用
Moq
中的
Setup
方法来设置模拟对象(
Mock
对象)中可重写方法的行为,结合
Returns
(用于返回一个值)或
Throws
(用于抛出异常)等方法来定义其行为。这样可以模拟对特定方法的调用,使其在测试中返回预期的值或抛出特定的异常。

创建
TestMockStaffController
测试类,接下来我们用
Moq
实现一下上面的例子

public class TestMockStaffController
{
    private readonly ITestOutputHelper _testOutputHelper;
    public TestMockStaffController(ITestOutputHelper testOutputHelper)
    {
        _testOutputHelper = testOutputHelper;
    }
    [Fact]
    public async Task AddStaff_WhenCalled_ReturnNoContent()
    {
        //Arrange
        var mock = new Mock<IStaffRepository>();

        mock.Setup(_ => _.AddStaffAsync(It.IsAny<Staff>(), default));
        var staffController = new StaffController(mock.Object);
        var staff = new Staff()
        {
            Age = 10,
            Name = "Test",
            Email = "Test@163.com",
            Created = DateTimeOffset.Now,
        };
        //Act
        var result = await staffController.AddStaff(staff);

        //Assert
        Assert.IsType<Results<NoContent, ProblemHttpResult>>(result);
    }

    [Fact]
    public async Task GetStaffById_WhenCalled_ReturnOK()
    {
        //Arrange
        var mock = new Mock<IStaffRepository>();
        var id = 1;
        mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default)).ReturnsAsync(() => new Staff()
        {
            Id = id,
            Name = "张三",
            Age = 18,
            Email = "zhangsan@163.com",
            Created = DateTimeOffset.Now
        });

        var staffController = new StaffController(mock.Object);

        //Act
        var result = await staffController.GetStaffById(id);

        //Assert
        Assert.IsType<Results<Ok<Staff>, NotFound>>(result);
        var okResult = (Ok<Staff>)result.Result;
        Assert.Equal(id, okResult.Value?.Id);
        _testOutputHelper.WriteLine(okResult.Value?.Name);

    }

    //先暂时省略后面测试方法....
}

看一下运行测试

image

Moq 核心功能讲解

通过我们上面这个简单的
Demo
简单的了解了一下 Moq 的使用,接下来我们对
Moq
和核心功能深入了解一下

通过安装的
Nuget
包可以看到,
Moq
依赖了
Castle.Core
这个包,
Moq
正是利用了
Castle
来实现动态代理模拟对象的功能。

基本概念

  • Mock
    对象:通过
    Moq
    创建的模拟对象,用于模拟外部依赖项的行为。

    //创建Mock对象
    var mock = new Mock<IStaffRepository>();
    
  • Setup
    :用于设置
    Mock
    对象的行为和返回值,以指定当调用特定方法时应该返回什么结果。

     //指定调用AddStaffAsync方法的参数行为
      mock.Setup(_ => _.AddStaffAsync(It.IsAny<Staff>(), default));
    

异步方法

从我们上面的单元测试中看到我们使用了一个异步方法,使用返回值
ReturnsAsync
表示的

  mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default))
       .ReturnsAsync(() => new Staff()
        {
            Id = id,
            Name = "张三",
            Age = 18,
            Email = "zhangsan@163.com",
            Created = DateTimeOffset.Now
        });

Moq
有三种方式去设置异步方法的返回值分别是:

  1. 使用 .Result 属性
    (Moq 4.16 及以上版本):


    • 在 Moq 4.16 及以上版本中,您可以直接通过
      mock.Setup
      返回任务的
      .Result
      属性来设置异步方法的返回值。这种方法几乎适用于所有设置和验证表达式。
    • 示例:
      mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);
  2. 使用 ReturnsAsync
    (较早版本):


    • 在较早版本的 Moq 中,您可以使用类似
      ReturnsAsync

      ThrowsAsync
      等辅助方法来设置异步方法的返回值。
    • 示例:
      mock.Setup(foo => foo.DoSomethingAsync()).ReturnsAsync(true);
  3. 使用 Lambda 表达式


    • 您还可以使用 Lambda 表达式来返回异步方法的结果。不过这种方式会触发有关异步 Lambda 同步执行的编译警告。
    • 示例:
      mock.Setup(foo => foo.DoSomethingAsync()).Returns(async () => 42);

参数匹配

在我们单元测试实例中用到了参数匹配,
mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default)).
,对就是这个
It.IsAny<int>()
,此处的用意是匹配任意输入的
int
类型的入参,接下来我们一起看下参数匹配的一些常用示例。

  • 任意值匹配
    It.IsAny<T>()

    mock.Setup(_ => _.GetStaffByIdAsync(It.IsAny<int>(), default))
    


  • ref 参数的任意值匹配:
    对于 ref 参数,可以使用 It.Ref
    .IsAny 进行匹配(需要 Moq 4.8 或更高版本)。

           //Arrange
         var mock = new Mock<IFoo>();
         // ref arguments
         var instance = new Bar();
         // Only matches if the ref argument to the invocation is the same instance
         mock.Setup(foo => foo.Submit(ref instance)).Returns(true);
    
    


  • 匹配满足条件的值:
    使用
    It.Is<T>(predicate)
    可以匹配满足条件的值,其中
    predicate
    是一个函数。

      //匹配满足条件的值
      mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true);
     //It.Is 断言
     var result = mock.Object.Add(3);
     Assert.False(result);
    


  • 匹配范围:
    使用
    It.IsInRange<T>
    可以匹配指定范围内的值

     mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Moq.Range.Inclusive))).Returns(true);
    var inRangeResult = mock.Object.Add(3);
    Assert.True(inRangeResult);
    


  • 匹配正则表达式:
    使用
    It.IsRegex
    可以匹配符合指定正则表达式的值

    {
      mock.Setup(x => x.DoSomethingStringy(It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("foo");
      var result = mock.Object.DoSomethingStringy("a");
      Assert.Equal("foo", result);
    }
    

属性值

  • 设置属性的返回值
    通过
    Setup
    后的
    Returns
    函数 设置
    Mock
    的返回值
     {
      mock.Setup(foo => foo.Name).Returns("bar");
      Assert.Equal("bar",mock.Object.Name);
     }
    


  • SetupSet
    设置属性的设置行为,期望特定值被设置.
    主要是通过设置预期行为,对属性值做一些验证或者回调等操作

      //SetupUp
       mock = new Mock<IFoo>();
       // Arrange
       mock.SetupSet(foo => foo.Name = "foo").Verifiable();
       //Act
       mock.Object.Name = "foo";
       mock.Verify();
    

如果值设置为
mock.Object.Name = "foo1";
,
单元测试就会抛出异常

OutPut:

 dotNetParadise.FakeTest.TestControllers.TestMockStaffController.Test_Moq_Demo
 源: TestMockStaffController.cs 行 70
 持续时间: 8.7 秒

消息: 
Moq.MockException : Mock<IFoo:2>:
This mock failed verification due to the following:

IFoo foo => foo.Name = "foo":
This setup was not matched.

堆栈跟踪: 
Mock.Verify(Func`2 predicate, HashSet`1 verifiedMocks) 行 309
Mock.Verify() 行 251
TestMockStaffController.Test_Moq_Demo() 行 666666
--- End of stack trace from previous location ---


  • VerifySet
    直接验证属性的设置操作
       //VerifySet直接验证属性的设置操作
       {
           // Arrange
           mock = new Mock<IFoo>();
           //Act
           mock.Object.Name = "foo";
           //Asset
           mock.VerifySet(person => person.Name = "foo");
       }


  • SetupProperty
    使用
    SetupProperty
    可以为
    Mock
    对象的属性设置行为,包括
    get

    set
    的行为。
 {
    // Arrange
     mock = new Mock<IFoo>();
      // start "tracking" sets/gets to this property
     mock.SetupProperty(f => f.Name);
      // alternatively, provide a default value for the stubbed property
     mock.SetupProperty(f => f.Name, "foo");
      //Now you can do:
     IFoo foo = mock.Object;
     // Initial value was stored
     //Asset
     Assert.Equal("foo", foo.Name);
 }


Moq
中,您可以使用
SetupAllProperties
方法来一次性存根(
Stub

Mock
对象的所有属性。这意味着所有属性都会开始跟踪其值,并可以提供默认值。以下是一个示例演示如何使用
SetupAllProperties
方法:

// 存根(Stub)Mock 对象的所有属性
mock.SetupAllProperties();

通过使用
SetupProperty
方法,可以更灵活地设置 Mock 对象的属性行为和默认值,以满足单元测试中的需求

处理事件(
Events


Moq
4.13 及以后的版本中,你可以通过配置事件的
add

remove
访问器来模拟事件的行为。这允许你指定当事件处理器被添加或移除时应该发生的逻辑。这通常用于验证事件是否被正确添加或移除,或者模拟事件触发时的行为。

  • SetupAdd
    用于设置
    Mock
    对象的事件的
    add
    访问器,即用于模拟事件订阅的行为
  • SetupRemove
    用于设置
    Mock
    对象的事件的
    remove
    访问器,以模拟事件处理程序的移除行为

创建要被测试的类:


public class HasEvent
{
    public virtual event Action Event;

    public void RaiseEvent() => this.Event?.Invoke();
}

        {
            var handled = false;
            var mock = new Mock<HasEvent>();
            //设置订阅行为
            mock.SetupAdd(m => m.Event += It.IsAny<Action>()).CallBase();
            // 订阅事件并设置事件处理逻辑
            Action eventHandler = () => handled = true;
            mock.Object.Event += eventHandler;
            mock.Object.RaiseEvent();
            Assert.True(handled);

            // 重置标志为 false
            handled = false;
            //  移除事件处理程序
            mock.SetupRemove(h => h.Event -= It.IsAny<Action>()).CallBase();
            // 移除事件处理程序
            mock.Object.Event -= eventHandler;
            // 再次触发事件
            mock.Object.RaiseEvent();

            // Assert -  验证事件是否被正确处理
            Assert.False(handled); // 第一次应该为 true,第二次应该为 false

        }

这段代码是一个针对
HasEvent
类的测试示例,使用 Moq 来设置事件的订阅和移除行为,并验证事件处理程序的添加和移除是否按预期工作。让我简单解释一下这段代码的流程:

  1. 创建一个 Mock 对象
    mock
    ,模拟
    HasEvent
    类。
  2. 使用
    SetupAdd
    方法设置事件的订阅行为,并使用
    CallBase
    方法调用基类的实现。
  3. 订阅事件并设置事件处理逻辑,将事件处理程序
    eventHandler
    添加到事件中。
  4. 调用
    RaiseEvent
    方法触发事件,并通过断言验证事件处理程序是否被正确处理。

  5. handled
    标志重置为
    false
  6. 使用
    SetupRemove
    方法设置事件的移除行为,并使用
    CallBase
    方法调用基类的实现。
  7. 移除事件处理程序
    eventHandler
  8. 再次触发事件,并通过断言验证事件处理程序是否被正确移除。

通过这个测试示例,可以验证事件处理程序的添加和移除操作是否正常工作

  • Raise
    Raise
    方法用于手动触发 Mock 对象上的事件,模拟事件的触发过程
        {
            // Arrange
            var handled = false;
            var mock = new Mock<HasEvent>();
            //设置订阅行为
            mock.Object.Event += () => handled = true;
            //act
            mock.Raise(m => m.Event += null);
            // Assert - 验证事件是否被正确处理
            Assert.True(handled);
        }

这个示例使用
Raise
方法手动触发
Mock
对象上的事件
Event
,并验证事件处理程序的执行情况。通过设置事件的订阅行为,触发事件,以及断言验证事件处理程序的执行结果,测试了事件处理程序的逻辑是否按预期执行。这个过程帮助我们确认事件处理程序在事件触发时能够正确执行.

Callbacks

Callback
方法用于在设置
Mock
对象的成员时指定回调操作。当特定操作被调用时,可以在
Callback
方法中执行自定义的逻辑

    //Arrange
    var mock = new Mock<IFoo>();
    var calls = 0;
    var callArgs = new List<string>();

    mock.Setup(foo => foo.DoSomething("ping"))
        .Callback(() => calls++)
       .Returns(true);

    // Act
    mock.Object.DoSomething("ping");

    // Assert
    Assert.Equal(1, calls); // 验证 DoSomething 方法被调用一次

在调用 DoSomething 方法是,回调操作自动被触发参数++


  • CallBack
    捕获参数
 //CallBack 捕获参数
 {
     //Arrange
     mock = new Mock<IFoo>();
     mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
         .Callback<string>(s => callArgs.Add(s))
         .Returns(true);
     //Act
     mock.Object.DoSomething("a");
     //Asset
     // 验证参数是否被添加到 callArgs 列表中
     Assert.Contains("a", callArgs);
 }

使用
Moq

Callback
方法可以捕获方法调用时的参数,允许我们在测试中访问和处理这些参数。通过在
Setup
方法中指定
Callback
操作,我们可以捕获方法调用时传入的参数,并在回调中执行自定义逻辑,例如将参数添加到列表中。这种方法可以帮助我们验证方法在不同参数下的行为,以及检查方法是否被正确调用和传递参数。总的来说,
Callback
方法为我们提供了一种灵活的方式来处理方法调用时的参数,帮助我们编写更全面的单元测试。


  • SetupProperty
    SetupProperty
    方法可用于设置
    Mock
    对象的属性,并为其提供
    getter

    setter
        {
            //Arrange
            mock = new Mock<IFoo>();
            mock.SetupProperty(foo => foo.Name);
            mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
                .Callback((string s) => mock.Object.Name = s)
                .Returns(true);
            //Act
            mock.Object.DoSomething("a");
            // Assert
            Assert.Equal("a", mock.Object.Name);
        }

SetupProperty
方法的作用包括:

  1. 设置属性的初始值
    :通过
    SetupProperty
    方法,我们可以设置
    Mock
    对象属性的初始值,使其在测试中具有特定的初始状态。

  2. 模拟属性的 getter 和 setter

    SetupProperty
    方法允许我们为属性设置
    getter

    setter
    ,使我们能够访问和修改属性的值。

  3. 捕获属性的设置操作
    :在设置
    Mock
    对象的属性时,可以使用
    Callback
    方法捕获设置操作,以执行自定义逻辑或记录属性的设置情况。

  4. 验证属性的行为
    :通过设置属性和相应的行为,可以验证属性的行为是否符合预期,以确保代码的正确性和可靠性

Verification


Moq
中,
Verification
是指验证
Mock
对象上的方法是否被正确调用,以及调用时是否传入了预期的参数。通过
Verification
,我们可以确保
Mock
对象的方法按预期进行了调用,从而验证代码的行为是否符合预期。

        {
            //Arrange
            var mock = new Mock<IFoo>();
            //Act
            mock.Object.Add(1);
            // Assert
            mock.Verify(foo => foo.Add(1));
        }


  • 验证方法被调用的行为
  • 未被调用,或者调用至少一次
   {
       var mock = new Mock<IFoo>();
       mock.Verify(foo => foo.DoSomething("ping"), Times.Never());
   }
mock.Verify(foo => foo.DoSomething("ping"), Times.AtLeastOnce());

Verify
指定
Times.AtLeastOnce()
验证方法至少被调用了一次。


  • VerifySet
    验证是否是按续期设置,上面有讲过。
  • VerifyGet
    用于验证属性的
    getter
    方法至少被访问指定次数,或者没有被访问.
    {
        var mock = new Mock<IFoo>();
         mock.VerifyGet(foo => foo.Name);
    }


  • VerifyAdd
    ,
    VerifyRemove

VerifyAdd

VerifyRemove
方法来验证事件的订阅和移除

// Verify event accessors (requires Moq 4.13 or later):
mock.VerifyAdd(foo => foo.FooEvent += It.IsAny<EventHandler>());
mock.VerifyRemove(foo => foo.FooEvent -= It.IsAny<EventHandler>());

  • VerifyNoOtherCalls

VerifyNoOtherCalls
方法的作用是在使用
Moq
进行方法调用验证时,确保除了已经通过
Verify
方法验证过的方法调用外,没有其他未验证的方法被执行

mock.VerifyNoOtherCalls();

Customizing Mock Behavior

  • MockBehavior.Strict
    使用
    Strict
    模式创建的
    Mock
    对象时,如果发生了未设置期望的方法调用,包括未设置对方法的期望行为(如返回值、抛出异常等),则在该未设置期望的方法调用时会抛出
    MockException
    异常。这意味着在
    Strict
    模式下,
    Mock
    对象会严格要求所有的方法调用都必须有对应的期望设置,否则会触发异常。
    [Fact]
    public void TestStrictMockBehavior_WithUnsetExpectation()
    {
        // Arrange
        var mock = new Mock<IFoo>(MockBehavior.Strict);
        //mock.Setup(_ => _.Add(It.IsAny<int>())).Returns(true);
        // Act & Assert
        Assert.Throws<MockException>(() => mock.Object.Add(3));
    }

如果
mock.Setup
这一行注释了,即未设置期望值,则会抛出异常


  • CallBase
    在上面的示例中我们也能看到
    CallBase
    的使用

    Moq
    中,通过设置
    CallBase = true
    ,可以创建一个部分模拟对象(
    Partial Mock
    ),这样在没有设置期望的成员时,会调用基类的实现。这在需要模拟部分行为并保留基类实现的场景中很有用,特别适用于模拟
    System.Web
    中的
    Web/Html
    控件。
public interface IUser
{
    string GetName();
}

public class UserBase : IUser
{
    public virtual string GetName()
    {
        return "BaseName";
    }

    string IUser.GetName() => "Name";
}

测试

    [Fact]
    public void TestPartialMockWithCallBase()
    {
        // Arrange
       var mock = new Mock<UserBase> { CallBase = true };
        mock.As<IUser>().Setup(foo => foo.GetName()).Returns("MockName");
        // Act
        string result = mock.Object.GetName();//

        // Assert
        Assert.Equal("BaseName", result);

        //Act
        var valueOfSetupMethod = ((IUser)mock.Object).GetName();
        //Assert
        Assert.Equal("MockName", valueOfSetupMethod);
    }

  • 第一个
    Act
    :调用模拟对象的 GetName() 方法,此时基类的实现被调用,返回值为
    "BaseName"

  • 第二个
    Act

本文分享自华为云社区《
Python 正则表达式大揭秘应用与技巧全解析
》,作者:柠檬味拥抱。

Python 中的 re 模块是用于处理正则表达式的强大工具。正则表达式是一种用来匹配字符串的模式,它可以在文本中搜索和匹配特定的字符串模式。在本文中,我们将探讨 Python 中 re 模块的应用和一些技巧,帮助您更好地理解和利用正则表达式。

1. 导入 re 模块

在开始之前,首先要确保已经导入了 re 模块:

import re

2. 使用 re 模块进行匹配

以下是一个简单的示例,演示如何使用 re 模块在字符串中查找特定模式的匹配项:

text = "The quick brown fox jumps over the lazy dog"# 使用 re 模块查找匹配项
matches
= re.findall(r'\b\w{3}\b', text)

print(matches) # 输出匹配的单词列表

在上面的示例中,我们使用了
\b\w{3}\b
这个正则表达式来匹配长度为3的单词。
\b
表示单词的边界,
\w{3}
表示匹配三个字母字符。
re.findall()
函数返回所有匹配的结果。

3. 使用分组

分组是正则表达式中的一个强大功能,它允许您对匹配的部分进行分组处理。以下是一个示例,演示如何使用分组从文本中提取出邮件地址:

text = "Contact us at: support@example.com, sales@example.com"# 使用分组提取邮件地址
emails
= re.findall(r'([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', text)

print(emails) # 输出提取的邮件地址列表

在上面的示例中,
([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})
是一个匹配邮件地址的正则表达式。其中,
()
将整个邮件地址作为一个分组,使得
re.findall()
函数只返回匹配的邮件地址部分。

4. 替换文本中的字符串

re 模块还提供了替换功能,允许您使用正则表达式来替换文本中的特定字符串。以下是一个示例,演示如何将文本中的所有数字替换为 “X”:

text = "There are 123 apples and 456 oranges"# 使用 re.sub() 函数替换文本中的数字为"X"new_text= re.sub(r'\d+', 'X', text)

print(new_text) # 输出替换后的文本

在上面的示例中,
re.sub(r'\d+', 'X', text)
使用正则表达式
\d+
匹配一个或多个数字,并将其替换为 “X”。

5. 使用编译的正则表达式

在处理大量文本时,编译正则表达式可以提高匹配效率。以下是一个示例,演示如何使用编译后的正则表达式进行匹配:

pattern = re.compile(r'\bpython\b', re.IGNORECASE)

text
= "Python is a popular programming language"# 使用编译后的正则表达式进行匹配
match
=pattern.search(text)ifmatch:
print(
"Found")else:
print(
"Not found")

在上面的示例中,
re.compile()
函数编译了一个不区分大小写的正则表达式,并且使用
search()
方法进行匹配。

通过掌握以上技巧,您可以更加灵活和高效地使用 Python 中的 re 模块进行正则表达式的处理。正则表达式是一项强大的技能,在处理文本和字符串时非常有用。

6. 使用预定义字符类

正则表达式中有一些预定义的字符类,可以简化匹配特定类型字符的操作。以下是一些常用的预定义字符类及其示例用法:

  • \d
    :匹配任意数字字符。
  • \w
    :匹配任意字母、数字或下划线字符。
  • \s
    :匹配任意空白字符(空格、制表符、换行符等)。
text = "The code is 1234 and the password is abcd_123"# 使用预定义字符类匹配数字和字母密码
codes
= re.findall(r'\b\w+\b', text)

print(codes) # 输出匹配的代码和密码列表

7. 使用量词

量词用于指定匹配字符或组的数量。以下是一些常用的量词及其示例用法:

  • *
    :匹配前一个字符零次或多次。
  • +
    :匹配前一个字符一次或多次。
  • ?
    :匹配前一个字符零次或一次。
  • {n}
    :匹配前一个字符恰好 n 次。
  • {n,}
    :匹配前一个字符至少 n 次。
  • {n,m}
    :匹配前一个字符至少 n 次,但不超过 m 次。
text = "The Python programming language is widely used for data analysis"# 使用量词匹配至少包含两个字母的单词
words
= re.findall(r'\b\w{2,}\b', text)

print(words) # 输出匹配的单词列表

8. 使用锚点

锚点用于匹配字符串的边界,而不是实际的字符。以下是一些常用的锚点及其示例用法:

  • ^
    :匹配字符串的开头。
  • $
    :匹配字符串的结尾。
  • \b
    :匹配单词的边界。
text = "Python is a great language for both beginners and experts"# 使用锚点匹配以 Python 开头的句子
sentence
= re.findall(r'^Python.*', text)

print(sentence) # 输出匹配的句子

9. 贪婪与非贪婪匹配

在正则表达式中,量词默认是贪婪的,即它们会尽可能匹配最长的字符串。但有时候我们希望匹配最短的字符串,这时候就需要使用非贪婪匹配。在量词后面加上
?
符号可以将其变为非贪婪匹配。

text = "Python is a powerful programming language"# 使用贪婪匹配查找"p""g"之间的内容
greedy_match
= re.findall(r'p.*g', text)

# 使用非贪婪匹配查找
"p""g"之间的内容
non_greedy_match
= re.findall(r'p.*?g', text)

print(
"贪婪匹配:", greedy_match) # 输出贪婪匹配结果
print(
"非贪婪匹配:", non_greedy_match) # 输出非贪婪匹配结果

10. 使用后向引用

后向引用允许您在正则表达式中引用先前匹配的内容。这在需要匹配重复的模式时非常有用。

text = "apple apple orange orange"# 使用后向引用匹配重复的单词
duplicates
= re.findall(r'(\b\w+\b) \1', text)

print(
"重复的单词:", duplicates) # 输出匹配到的重复单词列表

11. 多行匹配

有时候我们需要匹配多行文本,而不仅仅是单行。这时可以使用
re.MULTILINE
标志来启用多行匹配模式。

text = """Python is a popular programming language.
It is used forweb development, data analysis, and more.
Python has a simple syntax and
is easy to learn."""# 使用多行匹配模式匹配以大写字母开头的句子
sentences
= re.findall(r'^[A-Z].*$', text, re.MULTILINE)

print(
"以大写字母开头的句子:", sentences) # 输出匹配到的句子列表

12. 使用命名分组

在复杂的正则表达式中,为了增加可读性和维护性,可以使用命名分组来标识匹配的部分。

text = "John has 5 apples, Mary has 3 oranges"# 使用命名分组提取人名和水果数量
matches
= re.findall(r'(?P<name>\w+) has (?P<quantity>\d+) \w+', text)for match inmatches:
print(
"Name:", match['name'], "- Quantity:", match['quantity'])

以上是一些高级技巧,可以进一步扩展您对正则表达式的应用和理解。通过不断练习和尝试,您将能够更灵活地应用正则表达式来解决各种文本处理问题。

13. 使用预搜索断言

预搜索断言允许您在匹配字符串时,指定字符串之前或之后的条件。它不会消耗匹配的字符,仅用于指定条件。

text = "apple banana orange grape"# 使用预搜索断言匹配出包含"apple"之后的所有水果
result
= re.findall(r'(?<=apple\s)(\w+)', text)

print(
"包含 'apple' 之后的水果:", result) # 输出匹配到的水果列表

14. 使用肯定与否定预搜索断言

肯定预搜索断言
(?=...)
匹配满足条件的字符串,而否定预搜索断言
(?!)
匹配不满足条件的字符串。

text = "Python is a powerful programming language"# 使用肯定预搜索断言匹配包含"is"的单词
positive_result
= re.findall(r'\b\w+(?= is\b)', text)

# 使用否定预搜索断言匹配不包含
"is"的单词
negative_result
= re.findall(r'\b\w+(?! is\b)', text)

print(
"肯定预搜索断言:", positive_result) # 输出匹配到的单词列表
print(
"否定预搜索断言:", negative_result) # 输出匹配到的单词列表

15. 使用 re.finditer() 函数

re.finditer()
函数与
re.findall()
函数类似,但它返回一个迭代器,可以逐个访问匹配对象。

text = "Python is a powerful programming language"# 使用 re.finditer() 函数匹配所有单词
matches_iter
= re.finditer(r'\b\w+\b', text)for match inmatches_iter:
print(match.group()) # 输出匹配到的单词

16. 使用 re.split() 函数

除了匹配和查找文本模式,
re
模块还提供了
re.split()
函数,用于根据正则表达式模式拆分字符串。

text = "apple,banana,orange,grape"# 使用 re.split() 函数根据逗号拆分字符串
fruits
= re.split(r',', text)

print(
"拆分后的水果列表:", fruits) # 输出拆分后的水果列表

17. 使用 re.sub() 函数的替换函数参数

re.sub()
函数的第二个参数可以是一个函数,用于处理匹配的结果后再替换。

def double(match):return str(int(match.group(0)) * 2)

text
= "The numbers are 1, 2, 3, and 4"# 使用替换函数参数将所有数字乘以2
new_text
= re.sub(r'\d+', double, text)

print(
"替换后的文本:", new_text) # 输出替换后的文本

18. 使用 re.fullmatch() 函数

re.fullmatch()
函数用于检查整个字符串是否完全匹配给定的模式。

pattern = re.compile(r'\d{4}-\d{2}-\d{2}')

date1
= "2022-01-15"date2= "15-01-2022"# 使用 re.fullmatch() 函数检查日期格式
match1
=pattern.fullmatch(date1)
match2
=pattern.fullmatch(date2)ifmatch1:
print(
"日期格式正确")else:
print(
"日期格式错误")ifmatch2:
print(
"日期格式正确")else:
print(
"日期格式错误")

19. 使用 re.IGNORECASE 标志进行不区分大小写匹配

在编译正则表达式时,可以使用
re.IGNORECASE
标志来进行不区分大小写的匹配。

pattern = re.compile(r'python', re.IGNORECASE)

text
= "Python is a powerful programming language"# 使用不区分大小写匹配模式查找"Python"match=pattern.search(text)ifmatch:
print(
"Found")else:
print(
"Not found")

20. 使用 re.DEBUG 标志进行正则表达式调试

在编译正则表达式时,可以使用
re.DEBUG
标志来输出正则表达式的调试信息,以便更好地理解它的工作原理。

pattern = re.compile(r'\b\w{3}\b', re.DEBUG)

text
= "The quick brown fox jumps over the lazy dog"# 输出编译后的正则表达式调试信息
pattern.findall(text)

通过继续学习和实践这些高级的正则表达式技巧,您将能够更好地应用正则表达式来处理各种文本匹配和处理任务,提高代码的效率和可维护性。正则表达式是 Python 中强大而灵活的工具之一,对于处理字符串模式非常有用。

总结

通过本文的介绍,我们探索了 Python 中 re 模块的应用与技巧,使您能够更灵活和高效地处理正则表达式。我们从基础的模式匹配开始,介绍了如何使用 re 模块进行匹配、分组、替换等操作。随后,我们深入探讨了一些高级技巧,包括贪婪与非贪婪匹配、后向引用、多行匹配、预搜索断言等,这些技巧可以帮助您更好地处理复杂的文本处理任务。此外,我们还介绍了一些实用的函数和标志,如
re.split()

re.sub()
的替换函数参数、
re.fullmatch()

re.IGNORECASE

re.DEBUG
等,使您能够更灵活地应用正则表达式解决实际问题。

掌握正则表达式是 Python 编程中非常重要的一部分,它能够帮助我们更快地处理字符串模式匹配、文本提取等任务,提高代码的效率和可维护性。通过不断学习和实践,您将能够更深入地理解和应用正则表达式,解决各种文本处理问题,提升自己在 Python 编程中的技能水平。希望本文对您有所帮助,欢迎继续探索和学习更多关于正则表达式的知识。

点击关注,第一时间了解华为云新鲜技术~

本文深入探讨了Kubernetes Pod配置的实战技巧和常见易错点。

关注【TechLeadCloud】,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人

file

一、简介

Kubernetes基础概念回顾

在深入探讨Pod配置之前,让我们先快速回顾一下Kubernetes(K8s)的基础概念。Kubernetes是一个开源平台,旨在自动化容器化应用程序的部署、扩展和管理。它提供了一个可扩展的框架,允许用户运行分布式系统的应用程序而不必过分关注底层的硬件配置。

Kubernetes的关键组件包括但不限于:

  • 节点(Nodes)
    :集群的物理或虚拟机器。
  • Pods
    :最小的部署单位,每个Pod包含一个或多个容器。
  • 服务(Services)
    :定义了如何访问Pod,例如负载均衡和服务发现。
  • 部署(Deployments)
    :管理Pod的创建和更新。

理解这些基本概念对于深入理解Pod配置至关重要。

Pod的重要性与作用

Pod是Kubernetes中的基本构建块,是创建和管理的最小可部署单元。每个Pod通常封装一个应用容器(或有时多个紧密关联的容器),包括其存储资源、唯一的网络IP以及管理其运行方式的策略选项。

Pod的主要特点包括:

  • 共享资源
    :Pod内的容器共享相同的网络命名空间,包括IP地址和端口号,它们也可能共享存储。
  • 临时性
    :它们通常是短暂的,Kubernetes会在需要时创建和销毁Pods来保持应用程序的运行。
  • 多容器协作
    :Pod允许将多个容器放在一个逻辑单元中,这些容器可以紧密协作,共享资源并简化通信。

二、Pod配置基础

Pod的结构与配置文件概述

Pod是Kubernetes中的原子部署单位。理解Pod的结构对于高效地配置和管理Pod至关重要。一个基本的Pod配置文件包含了多个关键部分:

  • 元数据(Metadata)
    :包括Pod的名称、命名空间和标签,这些信息用于识别和组织Pod。
  • 规格(Spec)
    :定义了Pod的行为,比如运行哪些容器、使用哪些镜像、网络和存储配置等。
  • 状态(Status)
    :展示Pod的当前信息,如IP地址、运行状态等。

示例:基本Pod配置文件

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  labels:
    app: myapp
spec:
  containers:
  - name: my-container
    image: nginx

这个示例展示了一个最基础的Pod,包含一个使用nginx镜像的容器。

创建你的第一个Pod:步骤与示例代码

创建Pod的基本步骤通常包括:

  1. 编写Pod配置文件
    :根据你的应用需求编写YAML格式的配置文件。
  2. 使用kubectl创建Pod
    :使用命令
    kubectl apply -f <your-pod-file.yaml>
    来创建Pod。
  3. 验证Pod状态
    :使用
    kubectl get pods
    检查Pod的状态,确保它正在运行。

实际操作:部署一个简单的Pod

kubectl apply -f my-pod.yaml
kubectl get pods

这些命令首先创建一个Pod,然后列出所有Pod来检查新创建的Pod状态。

三、高级配置技巧

在掌握了Pod的基础配置之后,我们现在转向更高级和复杂的配置技巧。这些技巧旨在提高Pod的性能、安全性和灵活性,对于构建高效、可靠的Kubernetes环境至关重要。

资源限制与分配:Requests和Limits

在Kubernetes中,你可以为Pod中的每个容器指定资源请求(Requests)和限制(Limits)。这些设置确保了容器获得所需的资源,同时防止它们消耗过多资源,影响集群中的其他服务。

  • Requests
    :指定容器启动所需的最小资源量。如果请求的资源无法满足,容器不会被调度。
  • Limits
    :指定容器能使用的最大资源量。超过此限制可能导致容器被终止或重启。

示例:设置资源请求和限制

spec:
  containers:
  - name: my-container
    image: nginx
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

环境变量与ConfigMaps

环境变量是传递配置信息到Pod中的容器的一种方式。你可以在Pod的定义中直接设置环境变量,或者使用ConfigMaps来管理环境变量。

  • 直接设置环境变量
spec:
  containers:
  - name: my-container
    image: nginx
    env:
    - name: ENV_VAR_NAME
      value: "value"
  • 使用ConfigMaps

首先创建一个ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-config
data:
  ENV_VAR_NAME: "value"

然后在Pod配置中引用它:

spec:
  containers:
  - name: my-container
    image: nginx
    env:
    - name: ENV_VAR_NAME
      valueFrom:
        configMapKeyRef:
          name: my-config
          key: ENV_VAR_NAME

容器健康检查:Liveness和Readiness Probes

在Kubernetes中,Liveness Probes用于检测容器何时需要重启,而Readiness Probes用于检测容器何时准备好接收流量。

示例:Liveness和Readiness Probes

spec:
  containers:
  - name: my-container
    image: nginx
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 3
      periodSeconds: 3
    readinessProbe:
      httpGet:
        path: /readiness
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5

通过使用这些高级配置技巧,你可以提高Pod的性能,提升资源利用率,并确保你的应用更加稳定可靠。

四、Pod配置技巧与易错点

在Kubernetes中配置Pod时,了解一些高级技巧和常见的配置错误可以大大提高配置的效率和准确性。本章节将详细讨论一些关键的配置技巧以及在配置Pod时常见的易错点。

高级配置技巧

1. 利用Init Containers

Init容器在主应用容器启动之前运行,用于设置环境或执行预备任务。由于它们在应用容器之前运行,因此非常适合执行如数据迁移、环境准备等任务。

2. 使用Affinity和Anti-affinity

Pod的亲和性(affinity)和反亲和性(anti-affinity)规则允许你指定Pod应该或不应该在相同的节点或一组节点上与其他Pod共存。这对于高可用性和负载均衡配置至关重要。

3. 理解Graceful Shutdown

当Pod需要停止时,了解如何优雅地关闭它们是重要的。正确配置优雅关闭可以确保不丢失重要数据并维持服务可用性。

常见易错点

1. 错误配置资源限制

资源请求和限制的配置错误是最常见的问题之一。过高的资源限制可能导致资源浪费,而过低的限制可能导致应用性能问题。

2. 忽略Pod生命周期事件

不正确地处理Pod生命周期事件(如Liveness和Readiness Probes)可能导致服务中断。确保根据应用的具体需求调整这些探针的配置。

3. 配置错误的Volume Mounts

Volume挂载错误可能导致数据丢失或应用错误。确保正确配置并测试持久卷挂载点。

4. 未考虑Pod间的依赖关系

在有依赖关系的多个Pod间未设置正确的启动顺序会引发运行时错误。使用init容器或Pod依赖性规则来解决这些问题。

5. 配置过于复杂的网络规则

过度复杂的网络规则可能导致通信问题。尽可能简化网络配置,并确保理解Kubernetes的网络原理。

6. 忽视安全实践

在Pod配置中忽视安全设置,如不使用安全上下文(Security Contexts),可能导致安全漏洞。

通过注意这些配置技巧和易错点,你可以避免常见的陷阱,确保你的Kubernetes环境更加稳定和高效。下一章节将探讨Pod网络与通信的配置,进一步加深你对Kubernetes网络原理的理解。

如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

golang 的 embed 的功能真是一个很神奇的功能,它能把静态资源,直接在编译的时候,打包到最终的二进制程序中。

为什么会设计这么一个功能呢?我想和 golang 的崇尚简单的原则有关系吧。它希望的是一个二进制文件能走天下,那么如果你作为一个 web 服务器,还需要依赖一大堆的静态文件,终究不算是一人一天下,所以就提供了这么一种处理静态资源的办法。

golang 一旦提供了这种方式,可能会有哪些应用呢?我这里脑洞一下:

web 服务内嵌单页应用

目前的 web 应用很流行单页应用,基本上就一个 html+一个编译后的 js 就能搞定,那么我提供 web 服务器就直接通过 golang 编写,embed 方式将 html 和编译后的 js 内嵌到服务器中,那么就能很方便进行部署了。

app服务内嵌单页应用

同 web 服务一样,如果我使用 golang 写的是一个带有浏览器外框的 app 程序,内部使用 html+js进行渲染,那么岂不是这个服务就可以在手机/桌面端进行运行了?

可执行文件的版本管理

这下我们可以再仓库的根目录创建一个 version.txt, 里面填写上你的仓库的版本号,在项目中使用 embed 引入这个文件。如果你的项目是一个可执行文件,就能在执行的时候,直接显示出版本信息了。

可执行文件的git的commit显示

我们其实很希望知道我的这个可执行文件是对应 git 的哪个 commit,如果能将 .git 下的文件中的 commit 号直接 embed 到程序中就好了。

当然.git下的文件是不能直接 embed 的,但是这里提供了一种 generate+embed 的方式来实现:

Embedding Git Commit Information in Go Binaries

可执行文件的 readme

以前一个可执行文件在输入 help 的时候,需要显示一个信息内容,在 git 项目外也要有个 readme,其实两者都是对这个项目的帮助。那么现在,就能使用 embed 将两个合而为一了。

licence注入

我不确定golang 的二进制程序是否很容易被反编译,但是相较于简单的 licence 发放,将 licence 编译进入二进制程序已经是安全不少了。

我们要控制某个程序的发行,那么就在给用户编译二进制程序的时候,将对应的 licence 以 embed 的形式编译进入,然后在程序运行的时候,去远程或者本地使用非对称解密等方式来验证这个 licence 的合法性。

提高性能

我们可以将一些中间结果,比如 xxx 预计算模型啥的,以 embed 的形式内嵌进入程序。

程序运行的时候,就能将这些中间结构和预计算模型反序列化出来。

template文件进行embed

之前使用 golang 的 template 的时候,往往要创建一个很大的 template 的变量,而这个变量往往就是 html 或者 txt。现在就能独立将这个 html 或者 txt 放在 git 仓库中,在编译的时候 embed 进入。部署的时候直接使用了。

这样看代码的时候逻辑很清晰,运行的时候也很便捷。

参考

//go:embed 入门

Go 语言 | 1.16 新增的embed在各流行Web框架中的应用

道理我都懂,但 go embed 究竟该怎么用?

Go embed 简明教程

How can I embed hidden file in Go?

Embedding Git Commit Information in Go Binaries

技术背景

本文分享内容来自于最新的一篇名为
Multibody molecular docking on a quantum annealer
的文章,这篇文章的核心思想,是使用QUBO(二次受限二元优化)模型来求解一个分子对接问题:

分子对接

如果我们考虑空间中有
\(N\)
个分子,这
\(N\)
个分子可以摆放在任意的位置,以任意的角度。那么这些不同的位置和角度,每一个都可以计算出来一个能量值,不论是使用分子力场的方法,还是使用第一性原理计算的方法。而这其中每一个能量较低的位置组合,都是一个潜在的分子对接形式。两个分子的对接就叫两体对接,两个以上分子的对接可以称为多体对接。目前大多数的分子对接软件支持的还是两体对接,随着分子数量的增长,多体对接的计算难易度有可能是随着分子数量指数增长的。具体的复杂度跟建模方式有关,例如,在这篇文章中用到的二元建模方法,采样空间就是指数增长的。

QUBO模型

QUBO(quadratic unconstrained binary optimization formulation)模型常常被应用于组合优化问题中,其哈密顿量(损失函数)的通用形式为:

\[H_{QUBO}=\sum_ih_i(b_i)b_i+\sum_{i>j}J(b_i,b_j)b_ib_j
\]

其中
\(b_i\)
是一个二元变量
\(b_i\in\{0,1\}\)
。使用QUBO模型解决问题的基本思路,首先要把问题映射到一个二元变量上。举一个最简单的例子,背包问题(Knapspack Problem)的二元建模,有
\(M\)
个物体和1个背包,如果我们决定把第
\(j\)
个物体放到背包中,那么对应的二元变量
\(b_j=1\)
。相反地,如果我们决定不把第
\(j\)
个物体放到背包中,那么就有
\(b_j=0\)
。最终完成QUBO模型的求解之后,我们会得到一串二元字符串
\(000666666000...10100666666\)
,而这其中被标记为1的物体都会被放到背包里面。在我们这篇文章中要介绍的,就是如何使用QUBO模型来建模和求解多体分子对接的问题。

问题场景

这篇文章中求解的一个问题是三个
\(\alpha\)
螺旋的对接问题。因为主要是思想性的创新,所以用的体系简单一些,当然,这样也更加方便验证结果的正确性。而且做了一个降维的简化,实际上求解的是一个二维平面的对接问题:三个螺旋结构平行放置且没有错位:

QUBO建模

文章前面提到,我们要用QUBO模型来解决分子对接问题,首先要进行二元的建模,把问题转化为一个二元的模型。然后确定每一项所对应的系数,这样才能进入到模型的求解阶段。关于这个三螺旋的问题,QUBO模型研究的是这三个螺旋之间的两两相互作用(因为QUBO模型最高只能做到二次,对应的就是两体相互作用)。因为我们最终求解的目标是找到一些低能量的对接位点,因此我们可以启发性的使用相关的能量来作为系数。单点能好说,因为单个的分子不论放在什么位置,其能量只有内部结构决定。而两体相互作用能量(主要成分是范德华作用力)的参数需要结合QUBO建模来考虑。

这篇文章使用的方法是,对二维的空间进行打格点操作,把空间离散化。然后针对于每一个格点进行one-hot编码,例如说,2号螺旋在格点1而3号格点在格点2,那么对应的二元表述就是:

\[b_{(1-1)*2+0}=b_0=1;b_{(1-1)*2+1}=b_1=0\\
b_{(2-1)*2+0}=b_2=0;b_{(2-1)*2+1}=b_3=1
\]

之所以这里面one-hot只用到了2维,这是因为我们在处理三体问题时,其实可以把其中的一个螺旋作为中心点不动,因此实际上one-hot的维度是2维。这样一来,实际上我们的解要在
\(2^{2G}\)
的解空间中去寻找,这里
\(G\)
表示打的格点的数量,一般跟分子本身无关。可以类似的推导,如果是有
\(N\)
体的相互作用,那么最终解空间的大小就会是
\(2^{(N-1)G}\)
,也就是我们作为的指数级别增长。

确定了建模方法之后,我们可以通过计算分子的单点能和格点中的两两相互作用能量,来获得QUBO模型的系数:

约束条件

聪明的小伙伴已经意识到了一个问题,在上一个章节中的建模方法,会导致一个问题是,解空间中包含了两个分子处在相邻格点甚至是同一个格点内这样的解。还有可能出现一种更加不合理的情况:得到的解里面可能只剩下一个分子,会出现分子丢失的问题,这是Non-Physical的解。为了避免此类问题,需要在建模中加上一个惩罚值Penalty。因为这里加惩罚项的主要目的是为了避免分子被抹去的问题,因此约束条件可以这样定义:

\[\sum_ib_i^x=1
\]

这里
\(x\)
表示第
\(x\)
个分子,翻译成大白话就是:遍历所有的网格,你必然能找到且只能找到一个分子
\(x\)
。如果对所有的
\(x\)
加上这一项,那么就能尽可能的确保结果中不出现Non-Physical的解。不仅如此,一般情况下约束条件要双向约束,也就是过犹不及,因此一般都会采用二次形式的约束条件:

\[f(b^x)=(\sum_ib_i^x-1)^2=\sum_{i>j}2b_i^xb_j^x-\sum_ib_i^x+1=0
\]

要说明的是,这里约束条件只是作为一个目标,我们希望把
\(f(b^x)\)
这个约束条件优化到0,但实际上我们只能确保
\(f(b^x)\)
被加到QUBO模型完整的损失函数内的时候,得到完整的损失函数的最小值。

建模的最后,我们要得到带有惩罚值
\(\gamma\)
的最终哈密顿量(损失函数):

\[H_{MBD}=H_{QUBO}+\gamma f(b^x)=\sum_{x\in X'}\sum_{i=1}^{T}(h(b_i^x)-\gamma)b_i^x+\sum_{x\geq y}\sum_{i>j}(J(b_i^x,b_j^y)+\delta)b_i^xb_j^y
\]

其中
\(\delta\)
有:

\[\delta=\left\{
\begin{matrix}
2\gamma, x=y\\
0, otherwise
\end{matrix}
\right.
\]

因为我们建模的时候固定了一个中心的
\(\alpha\)
螺旋,所以建模中的约束条件实际上是针对于可移动的单体目标的,这个集合被设定为
\(X'\)

求解QUBO模型

当QUBO建模完成后,QUBO模型的求解过程是固定的,目前而言有三种主流的方法:使用退火机求解、使用模拟退火算法求解、使用QAOA算法求解,其中QAOA算法相当于退火算法的一个离散化版本,但是因为可以在量子计算机上面进行求解,所以退火机求解和QAOA求解算法可以重点关注下。而这篇文章里面用到的是退火机和模拟退火,退火机使用的是加拿大公司Dwave的硬件,亚洲地区用光学做的退火机可能更多一些。

退火的大致原理,就是构造一个简单的哈密顿量
\(H_0\)
和对应的本征态
\(|{\psi_0}\rangle\)
,以及目标哈密顿量
\(H_{MBD}\)
和目标量子态
\(|{\psi_{MBD}}\rangle\)
,通过准静态过程实现
\(H_{t}=\lambda H_0+(1-\lambda)H_{MBD}\)
。根据绝热近似(adiabatic approximation)理论,我们最终可以制备出来目标哈密顿量的量子态,从而实现对目标哈密顿量的求解。在前面的文章中我们实现过
使用绝热演化/量子退火算法求解矩阵本征态

以及
量子绝热算法求解最大切割问题

,感兴趣的读者可以跳转去看一下退火算法的相关实现,这里不做赘述。

总而言之我们最终会得到一个本征量子态。而所谓的量子态在这个问题求解中的含义,其实对应的是一个求解空间的子空间。在这个子空间中进行采样,有更高的概率可以获得最终的最优解,并不是说这个量子态就是最终解了。而如果是通过退火机和量子计算机的体系来实现的话,可以做到高效的采样,从而做到更加快速的得到最优解。在我们引用的这篇文献中,重点介绍了一下惩罚值对于采样结果的影响,从而指导采样结果的选取。其实对于更多的工程问题而言,惩罚值的选取更多的依赖于经验,是一个经验参数。

文献里面的基本结论是这样的:如果惩罚值选取的过小,那么没办法区分开Physical的解和Non-Physical的解;如果惩罚值过高,则很难采样采到能量较低(较优)的Physical解;因此,适中的惩罚值会是一个更好的选择:

文献中还有一些更进一步的测试结果,还包含了退火机和模拟退火算法的效果比对,感兴趣的读者可以自行下载文献进行阅读。

总结概要

本文主要分享了文献Multibody molecular docking on a quantum annealer中的主要建模思路和初步的测试结果,可以实现QUBO模型求解多体分子对接的问题。在目前常见的分子对接软件中,更多的是实现的两体对接,多体对接的采样空间有可能会随着分子数量的增长而指数增长。而借助于量子退火机或者是量子计算机来求解这样的一个问题,不失为一个较好的思路。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/qubo_docking.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

参考文献

  1. "
    Multibody molecular docking on a quantum annealer
    ". Mohit Pandey, Tristan Zaborniak, Hans Melo, Alexey Galda, and Vikram Khipple Mulligan