草庐IT

unit-testing - 当A的方法在Go中返回B时模拟对象A和B

coder 2024-07-11 原文

我正在尝试在 Go 中为现有服务实现单元测试,该服务使用连接池结构和来自现有库的连接结构(调用这些 LibraryPoolLibraryConnection) 连接到外部服务。 为了使用这些,主代码中的服务函数使用池的一个唯一的全局实例,它有一个 GetConnection() 方法,如下所示:

// Current Main Code
var pool LibraryPool // global, instantiated in main()

func someServiceFunction(w http.ResponseWriter, r *http.Request) {
  // read request
  // ...
  conn := pool.GetConnection()
  conn.Do("some command")
  // write response
  // ... 
}

func main() {
  pool := makePool() // builds and returns a LibraryPool
  // sets up endpoints that use the service functions as handlers
  // ...
}

我想在不连接到外部服务的情况下对这些服务功能进行单元测试,因此我想模拟 LibraryPool 和 LibraryConnection。为此,我正在考虑将主要代码更改为如下内容:

// Tentative New Main Code
type poolInterface interface {
  GetConnection() connInterface
}

type connInterface interface {
  Do(command string)
}

var pool poolInterface

func someServiceFunction(w http.ResponseWriter, r *http.Request) {
  // read request
  // ...
  conn := pool.GetConnection()
  conn.Do("some command")
  // write response
  // ...
}

func main() {
  pool := makePool() // still builds a LibraryPool
}

在测试中,我将使用这些接口(interface)的模拟实现 MockPoolMockConnection,全局 pool 变量将使用 模拟池。我将在 TestMain() 内的 setup() 函数中实例化这个全局 pool功能。

问题是在新的主代码中,LibraryPool 没有正确实现poolInterface,因为GetConnection() 返回 connInterface 而不是 LibraryConnection(即使 LibraryConnectionconnInterface 的有效实现)。

进行此类测试的好方法是什么?顺便说一句,主要代码也很灵活。

最佳答案

好吧,我会尝试通过完整解释我如何看待这个设计来回答。如果这太多而不是重点,请提前道歉..

  • 实体/域
    • 应用程序的核心,将包含实体结构,不会导入任何外层包,但每个包都可以导入(几乎)
  • 应用程序/用例
    • “服务”。将主要负责应用程序逻辑,不知道传输(http),将通过接口(interface)与数据库“对话”。您可以在此处进行域验证,例如,如果找不到资源或文本太短。与业务逻辑相关的任何内容。
  • 运输
    • 将处理 http 请求、解码请求、让服务执行他的工作并对响应进行编码。如果请求中缺少必需的参数,或者用户未被授权,或者其他原因,您可以在这里返回 401...
  • 基础设施
    • 数据库连接
    • 也许是一些 http 引擎和路由器之类的东西。
    • 完全与应用程序无关,不导入任何内部包,甚至不导入 Pseron

例如,假设我们想做一些像将人插入数据库一样简单的事情。

包 person 将只包含 person 结构

package person

type Person struct{
  name string
}

func New(name string) Person {
  return Person{
    name: name,
  {
}

关于数据库,假设你使用sql,我建议制作一个名为sql的包来处理repo。 (如果你使用 postgress,请使用'postgress package ...)。

personRepo 将获取将在 main 中初始化的 dbConnection 并实现 DBAndler。只有连接会直接与数据库“对话”,存储库的主要目标是成为数据库的网关,并以应用程序的方式说话。 (连接与应用无关)

package sql

type DBAndler interface{
  exec(string, ...interface{}) (int64, error)
}

type personRepo struct{
  dbHandler DBHandler
}

func NewPersonRepo(dbHandler DBHandler) &personRepo {
  return &personRepo{
    dbHandler: dbHandler,
  }
}

func (p *personRepo) InsertPerson(p person.Person) (int64, error) {
  return p.dbHandler.Exec("command to insert person", p)
}

服务将在初始化程序中将此存储库作为依赖项(作为接口(interface))获取,并将与其交互以完成业务逻辑

package service

type PersonRepo interface{
  InsertPerson(person.Person) error
}

type service struct {
  repo PersonRepo
}

func New(repo PersonRepo) *service {
  return &service{
    repo: repo
  }
}

func (s *service) AddPerson(name string) (int64, error) {
  person := person.New(name)
  return s.repo.InsertPerson(person)
}

您的传输处理程序将使用作为依赖项的服务进行初始化,并且他将处理 http 请求。

package http

type Service interface{
  AddPerson(name string) (int64, error)
}

type handler struct{
  service Service
}

func NewHandler(s Service) *handler {
  return &handler{
    service: s,
  }
}

func (h *handler) HandleHTTP(w http.ResponseWriter, r *http.Request) {
  // read request
  // decode name

  id, err := h.service.AddPerson(name)

  // write response
  // ... 
}

在 main.go 中你会把所有东西联系在一起:

  1. 初始化数据库连接
  2. 使用此连接初始化 personRepo
  3. 使用 repo 初始化服务
  4. 使用服务初始化传输

主要包

func main() {
  pool := makePool()
  conn := pool.GetConnection()

  // repo
  personRepo := sql.NewPersonRepo(conn)

  // service
  personService := service.New(personRepo)

  // handler
  personHandler := http.NewPersonHandler(personService)

  // Do the rest of the stuff, init the http engine/router by passing this handler.

}

请注意,每个包结构都使用 interface 初始化,但返回一个 struct,并且接口(interface)是在使用它们的包中声明的,而不是在使用它们的包中声明的实现它们。

这使得对这些包进行单元测试变得容易。例如,如果您想测试服务,则无需担心 http 请求,只需使用一些实现服务所依赖的接口(interface)(PersonRepo)的“模拟”结构,就可以了。

好吧,我希望它对你有一点帮助,一开始它可能看起来很困惑,但你很快就会发现这看起来像是一大段代码,但当你需要添加功能或切换db 驱动程序等。我建议您阅读 go 中的域驱动设计,以及六角拱。

编辑:

此外,通过这种方式您传递到服务的连接,服务不会导入和使用全局数据库池。老实说,我不知道为什么它如此普遍,我想它有它的优点并且对某些应用程序更好,但一般来说我认为让你的服务依赖于某个接口(interface),而不真正知道发生了什么,是很多更好的做法。

关于unit-testing - 当A的方法在Go中返回B时模拟对象A和B,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53769424/

有关unit-testing - 当A的方法在Go中返回B时模拟对象A和B的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  5. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  6. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  7. Ruby 方法() 方法 - 2

    我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

  8. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

    为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

  9. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  10. ruby - Highline 询问方法不会使用同一行 - 2

    设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案

随机推荐