Golang中两个Go协程操作单个共享模拟仓库导致测试失败

Golang中两个Go协程操作单个共享模拟仓库导致测试失败 我正在使用testify为我的服务编写单元测试。在那里,我模拟了我的仓库,并尝试通过我正在测试的服务调用其函数。

我的模拟

import (
	....
	....
	"github.com/stretchr/testify/mock"
)

type MyRepository struct {
	mock.Mock
}

func (m *MyRepository) FindAll(c cri.Criteria) ([]models.Cinema, error) {
	args := m.Called()
	if args.Error(1) != nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.Cinema), nil
}

我正在尝试测试的服务

func (s *MyService) GetAll(context context.Context) (*dto.DtoList, error) {
	models, err := s.repository.FindAll(cri.NilCriteria())
	if err != nil {
		return nil, err
	}

	return dto.GetDtoList(models), nil
}

我的测试文件

import (
	....
	.....
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/suite"
)

type MyServiceTestSuite struct {
	suite.Suite
	myRepo    *mockRepository.MyRepository
	myService *CinemaService
}

func (suite *MyServiceTestSuite) SetupSuite() {

	suite.T().Log("Setting up test suite")

	suite.myRepo = &mockRepository.MyRepository{}

	...
	...

	suite.myService = NewMyService(suite.myRepo, suite.config)

}

func (suite *MyServiceTestSuite) TestMyService_GetAll() {

	suite.Run("Return All when no error", func() {

		suite.myRepo.On("FindAll").Return([]models.Cinema{cinemaModel1, cinemaModel2}, nil)
		allCinema2, err2 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.ElementsMatch(suite.T(), expectedList.Cinemas, allCinema2.Cinemas)
		assert.Equal(suite.T(), expectedList.Cinemas[0].Id, allCinema2.Cinemas[0].Id)
		assert.Nil(suite.T(), err2)

	})

	suite.Run("Return Error when repo returns error", func() {

		noDataErr := errors.New("No Data")
		suite.myRepo.On("FindAll").Return(nil, noDataErr)
		allCinema1, err1 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.Nil(suite.T(), allCinema1)
		assert.Equal(suite.T(), noDataErr, err1)

	})

}

现在我的问题是,当我执行测试时,它失败了。问题出在名为“Return Error when repo returns error”的第二个子测试中。尽管我期望返回nil和错误,但它却返回了一个影院列表和一个nil错误。基本上,这是名为“Return All when no error”的子测试的输出。

我不理解这种行为。但我感觉共享的myRepo与此有关。但为什么会发生这种情况?是因为两个子测试并行运行吗?有没有一种方法可以在不为每个子测试使用单独的myRepo实例的情况下解决这个问题?

如果可能的话,请向我解释原因和解决方法。我想我遗漏了一些关于goroutine的基本概念。


更多关于Golang中两个Go协程操作单个共享模拟仓库导致测试失败的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中两个Go协程操作单个共享模拟仓库导致测试失败的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题在于testify/suite默认并行运行子测试,而你的模拟仓库在多个goroutine中被共享使用。当第一个子测试设置FindAll返回数据时,第二个子测试的调用也匹配到了同一个模拟设置。

原因分析

suite.Run()创建的每个子测试都会并行执行,它们共享同一个suite.myRepo实例。当第一个子测试调用FindAll()时,它会消耗掉模拟设置,但第二个子测试的调用仍然匹配到第一个子测试的设置,因为:

  1. 模拟的On("FindAll")调用没有参数匹配器
  2. 两个子测试都调用无参数的FindAll()
  3. 模拟设置是全局的,不是按测试隔离的

解决方案

方案1:重置模拟在每个子测试前(推荐)

func (suite *MyServiceTestSuite) TestMyService_GetAll() {

	suite.Run("Return All when no error", func() {
		// 重置模拟状态
		suite.myRepo.ExpectedCalls = nil
		suite.myRepo.Calls = nil
		
		suite.myRepo.On("FindAll").Return([]models.Cinema{cinemaModel1, cinemaModel2}, nil)
		allCinema2, err2 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.ElementsMatch(suite.T(), expectedList.Cinemas, allCinema2.Cinemas)
		assert.Equal(suite.T(), expectedList.Cinemas[0].Id, allCinema2.Cinemas[0].Id)
		assert.Nil(suite.T(), err2)
	})

	suite.Run("Return Error when repo returns error", func() {
		// 重置模拟状态
		suite.myRepo.ExpectedCalls = nil
		suite.myRepo.Calls = nil
		
		noDataErr := errors.New("No Data")
		suite.myRepo.On("FindAll").Return(nil, noDataErr)
		allCinema1, err1 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.Nil(suite.T(), allCinema1)
		assert.Equal(suite.T(), noDataErr, err1)
	})
}

方案2:使用SetupTest方法

func (suite *MyServiceTestSuite) SetupTest() {
	// 每个测试运行前重置模拟
	suite.myRepo.ExpectedCalls = nil
	suite.myRepo.Calls = nil
}

func (suite *MyServiceTestSuite) TestMyService_GetAll() {
	suite.Run("Return All when no error", func() {
		suite.myRepo.On("FindAll").Return([]models.Cinema{cinemaModel1, cinemaModel2}, nil)
		allCinema2, err2 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.ElementsMatch(suite.T(), expectedList.Cinemas, allCinema2.Cinemas)
		assert.Equal(suite.T(), expectedList.Cinemas[0].Id, allCinema2.Cinemas[0].Id)
		assert.Nil(suite.T(), err2)
	})

	suite.Run("Return Error when repo returns error", func() {
		noDataErr := errors.New("No Data")
		suite.myRepo.On("FindAll").Return(nil, noDataErr)
		allCinema1, err1 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.Nil(suite.T(), allCinema1)
		assert.Equal(suite.T(), noDataErr, err1)
	})
}

方案3:禁用子测试并行执行

func (suite *MyServiceTestSuite) TestMyService_GetAll() {
	// 串行执行子测试
	suite.Run("Return All when no error", func() {
		suite.T().Parallel()
		suite.myRepo.On("FindAll").Return([]models.Cinema{cinemaModel1, cinemaModel2}, nil)
		allCinema2, err2 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.ElementsMatch(suite.T(), expectedList.Cinemas, allCinema2.Cinemas)
		assert.Equal(suite.T(), expectedList.Cinemas[0].Id, allCinema2.Cinemas[0].Id)
		assert.Nil(suite.T(), err2)
	})

	suite.Run("Return Error when repo returns error", func() {
		suite.T().Parallel()
		noDataErr := errors.New("No Data")
		suite.myRepo.On("FindAll").Return(nil, noDataErr)
		allCinema1, err1 := suite.myService.GetAll(context.Background())
		suite.myRepo.AssertExpectations(suite.T())

		assert.Nil(suite.T(), allCinema1)
		assert.Equal(suite.T(), noDataErr, err1)
	})
}

关键点

  1. testify/mock的模拟设置是累积的,不会自动清除
  2. testify/suite默认并行运行子测试
  3. 共享的模拟状态需要在测试间重置
  4. 使用ExpectedCalls = nilCalls = nil可以完全重置模拟状态

方案1或方案2是最佳实践,确保每个测试有干净的模拟状态。

回到顶部