mcpField
S
spree-testing

spree-testing

github.com/spree测试与质量10224 次安装4 周前更新
代码仓库

关于 spree-testing

spree-testing 是面向 Spree 应用和扩展开发者的测试指南,介绍 Rails、仪表板和浏览器测试工具,并提供工厂、工作流、API 测试及常见故障示例。

主要功能

  1. 01RSpec 与 Factory Bot 配置
  2. 02Spree 模型和结账工厂
  3. 03工作流、钩子与订阅者测试
  4. 04API v3 共享上下文和示例
  5. 05仪表板的 Vitest 与 Playwright 测试
  6. 06扩展的 Dummy 应用测试

适用场景

  1. 01为 Spree 应用添加模型和工作流测试
  2. 02测试 API v3 身份验证与权限
  3. 03用浏览器测试覆盖仪表板行为
  4. 04在 Dummy 应用中运行扩展测试套件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/spree/agent-skills --skill spree-testing

安装后在 Claude Code 中输入/spree-testing确认已加载。

详细介绍

Spree 测试

层级工具
Rails 应用(server/)RSpec + Factory Bot + spree_dev_tools——不使用 Minitest,也不使用 fixtures
控制面板(apps/dashboard)Vitest(单元测试)+ Playwright(端到端测试),已预先配置
Storefront(apps/storefront)使用 Next.js 应用自带的工具

Spree 没有服务器端渲染的管理界面或 Storefront,因此无需为 Spree 界面编写 Capybara 功能规格。浏览器测试应使用 Playwright 覆盖控制面板和 Storefront。

设置

通过 create-spree-app 创建的项目已配置 RSpec。对于传统应用:

# Gemfile
group :development, :test do
  gem 'spree_dev_tools'
end
bin/rails g spree_dev_tools:install

如有需要,生成器会运行 rspec:install,并写入 spec/support/{spree,factory_bot,database_cleaner,active_job}.rb。spec/support/spree.rb 会加载:

  • Spree 的工厂(spree/testing_support/factories)+ FactoryBot 语法
  • spree/testing_support/store——默认 USD 店铺(@default_store),在每个测试组前创建,并在每个示例后清理
  • Spree::TestingSupport::Preferences(在每个示例前运行 reset_spree_preferences)
  • spree/api/testing_support/v3/base——API v3 共享上下文和 JWT 帮助方法
  • 用于 type: :request 和 type: :controller 的 json_response
  • ActiveJob::TestHelper、DatabaseCleaner(事务策略)

spec/factories/spree/**/*_decorator.rb 中的工厂装饰器会自动加载。

运行

spree rspec                                       # whole suite, in the web container, RAILS_ENV=test
spree rspec spec/models/spree/brand_spec.rb:15    # file / line; any rspec flags pass through
bundle exec rspec …                               # classic app, from the app root

在全新环境中,spree rspec 也能运行(会回退到启动一次性容器)。在传统应用中添加迁移后,运行:bin/rails db:test:prepare。

常用工厂

如有疑问,请查看源码:$(bundle show spree_core)/lib/spree/testing_support/factories/。

需求工厂
客户 / 员工:customer(别名为 :user)、:customer_with_addresses、:admin_user(会获得管理员角色;提供 :without_admin_role trait)
商品:product(默认变体以店铺货币定价为 19.99)、:product_in_stock、:product_with_option_types、:digital_product;瞬态属性:price:、compare_at_price:、currency:
变体:variant(相同的价格瞬态属性)、trait :with_no_price、:on_demand_variant
购物车(Spree::Cart):cart、:cart_with_line_items(line_items_count:)、:cart_ready_for_delivery(地址 + 配送方案)、:cart_ready_to_complete(付款金额足以支付总额)
订单(Spree::Order):order(草稿)、:order_with_line_items、:completed_order_with_totals(已下单)、:completed_order_with_pending_payment、:order_ready_to_ship、:shipped_order
履约:fulfillment(别名为 :shipment)、:delivery_method(别名为 :shipping_method)、:free_delivery_method、:pickup_delivery_method、:digital_delivery_method
库存:stock_location、:stock_level
付款:payment、:check_payment_method、:credit_card_payment_method、:store_credit_payment_method
目录:category、:collection、:automatic_collection、:custom_field_definition
退货:return、:approved_return、:received_return
API / 权限:api_key(:publishable、:secret → 作用域 ['write_all']、:revoked)、:role(permissions: %w[read_orders])

结账状态通过工厂创建,而不是通过逐步操作流程创建。OrderWalkthrough.up_to(...) 已弃用(在 6.1 中移除)——请使用 :cart_ready_for_delivery、:cart_ready_to_complete 或 :completed_order_with_totals。

购物车和订单是不同的模型:购物车没有状态;完成购物车后会创建一个具有 status(draft/placed/canceled)的 Order。如需测试实际完成流程:

cart = create(:cart_ready_to_complete)
result = Spree.carts_complete_workflow.call(cart: cart)

expect(result).to be_success
order = result.value
expect(order).to be_placed

价格按货币区分——不存在 variant.price。请使用 variant.price_in('EUR')&.amount / variant.amount_in('EUR') 进行断言;使用 create(:variant, price: 10, currency: 'EUR') 或 variant.set_price('EUR', 10) 设置价格。

对于自定义资源:spree generate api_resource Brand … 会生成 spec/factories/spree/brand_factory.rb,以及 Store/Admin 控制器规格。

存储上下文和必需关联

  • 在请求之外运行代码的规格(工作流、作业、服务)中,如果存储不是默认存储,请设置 Spree::Current.store。Spree::Current.store 会回退到 Spree::Store.default;如果不存在默认存储,该值为 nil,此时存储范围内的记录会报错:"Store must exist"。标记为 without_global_store: true 的组会完全跳过默认存储。
  • Spree 模型上的 belongs_to 是必需关联。模型的工厂必须构建每个非可选关联;失败信息为 "<Assoc> must exist"(而不是 "can't be blank")。如果允许为空,请在关联上声明 optional: true。
  • Spree::Current 是 ActiveSupport::CurrentAttributes 的子类;rspec-rails 会在各示例之间重置它,因此请在示例内部(或 before 中)设置。
  • 设置 Spree::Current.store 会在本示例剩余时间内启用 Spree::StoreScopeGuard:对既非存储范围查询、也非按 ID 筛选的存储所属表执行的任何 SELECT,都会被记录(默认 log 模式);如果设置 SPREE_STORE_SCOPE_GUARD=raise,则会引发 Spree::StoreScopeGuard::UnscopedQueryError(Spree 自身的 API 测试套件使用此设置,值得在 CI 中启用)。API v3 请求/控制器规格始终受保护。请修正查询(store.products…),或将有意进行的全局查找包装在 Spree::StoreScopeGuard.skip { … } 中。
  • 扩展的 dummy 应用(rake test_app)会配置 Active Record 加密密钥,因此加密列(webhook 密钥、身份令牌)的行为与生产环境一致。宿主应用的测试环境也需要配置自己的密钥,才能获得相同的行为。

模型规格

require 'rails_helper'

RSpec.describe Spree::Brand, type: :model do
  it 'rejects a duplicate slug in the same store' do
    create(:brand, slug: 'nike')
    expect(build(:brand, slug: 'nike')).not_to be_valid
  end
end

不需要持久化时,优先使用 build。测试自定义逻辑,而不是 Rails 的存在性验证、强参数或普通关联。

工作流和钩子规格

工作流会返回一个结果:success?、value、error。

RSpec.describe MyApp::RequirePoNumber do        # a 'carts.complete.validate' handler
  after { Spree.hooks.unregister('carts.complete.validate', described_class) }

  it 'vetoes completion without a PO number' do
    Spree.hooks.register('carts.complete.validate', described_class)
    cart = create(:cart_ready_to_complete)

    result = Spree.carts_complete_workflow.call(cart: cart)

    expect(result).not_to be_success
    expect(result.error.to_s).to include('PO number')
  end
end

只注销你注册的内容——Spree.hooks.clear! 也会移除核心自身的注册项(例如退货资格验证器),并影响进程的剩余部分。钩子键来自工作流(hooks :validate, …);在启用 eager loading 时,未知键会导致启动失败。

要单独对处理器进行单元测试,请传入一个 double,或它所读取的真实工作流实例(flow.cart、flow.reject!(msg))。

订阅者规格

订阅者生成器会生成一个规格;模式如下:

RSpec.describe OrderPlacedSubscriber do
  let(:order) { create(:completed_order_with_totals) }
  let(:event) { Spree::Event.new(name: 'order.placed', payload: { 'id' => order.prefixed_id }) }

  it 'submits the order' do
    expect(WarehouseClient).to receive_message_chain(:new, :submit).with(order)
    described_class.new.handle(event)
  end
end

直接调用 handle,不要通过事件总线来测试处理器。要断言某项操作会发布事件:

expect(Spree::Events).to receive(:publish).with('brand.created', anything, anything).and_call_original
brand.save!

对于使用 publishes_lifecycle_events 的模型,请复用 Spree 的共享示例:

require 'spree/testing_support/lifecycle_events'

RSpec.describe Spree::Brand do
  it_behaves_like 'lifecycle events'           # factory: :brand, event_prefix: 'brand' inferred
end

如果你的测试套件全局禁用了事件(Spree 自身的测试套件就是如此,使用 Spree::Events.disable!),请在每个示例中使用 Spree::Events.enable { … } 重新启用。

API v3 控制器规范

共享上下文(来自 spree/api/testing_support/v3/base,已由 spree_dev_tools 加载):

上下文提供内容
'API v3 Store'store、可发布的 api_key、api_key_headers、user(客户)、jwt_token、bearer_headers;桩替换 current_store
'API v3 Store guest' / 'API v3 Store authenticated'上述内容 + headers(仅密钥 / 密钥 + 客户 JWT)
'API v3 Admin'store、secret_api_key(write_all)、admin_user、admin_jwt_token、bearer_headers
'API v3 Admin authenticated'上述内容 + headers
'API v3 Admin with custom permissions'设置 let(:custom_permissions) { %w[read_orders] } — 仅包含这些键的员工 JWT
'API v3 Seller' / 'API v3 Seller authenticated'卖家、卖家用户、JWT、X-Spree-Seller-Id 请求头

共享示例:'returns 200 OK'、'returns 201 Created'、'returns 204 No Content'、'returns 401 Unauthorized'、'returns 403 Forbidden'、'returns 404 Not Found'、'returns 422 Unprocessable Entity'、'requires API key'、'requires authentication'(均要求有 subject)。

RSpec.describe Spree::Api::V3::Admin::BrandsController, type: :controller do
  render_views
  routes { Spree::Core::Engine.routes }        # API controllers are mounted by the engine
  include_context 'API v3 Admin authenticated'
  before { request.headers.merge!(headers) }

  let!(:brand) { create(:brand) }

  describe 'GET #index' do
    subject { get :index, params: { q: { name_cont: brand.name[0, 3] } }, as: :json }

    it_behaves_like 'returns 200 OK'

    it 'returns prefixed ids' do
      subject
      expect(json_response['data'].map { |b| b['id'] }).to include(brand.prefixed_id)
    end
  end

  context 'with a role lacking write_brands' do
    include_context 'API v3 Admin with custom permissions'
    let(:custom_permissions) { %w[read_brands] }

    subject { post :create, params: { name: 'X' }, as: :json }

    it_behaves_like 'returns 403 Forbidden'
  end
end
  • 始终断言带前缀的 ID(brand.prefixed_id),不要使用整数 ID。
  • 生成器的规范正是采用这种结构 — 在其基础上扩展,不要从头开始。
  • 你自己的资源的权限键(read_<resource>/write_<resource>)仅在 Spree.permissions.register_scope 后才存在 — 请参阅 spree-auth-permissions。

Dashboard 测试

cd apps/dashboard
pnpm test          # vitest run
pnpm test:e2e      # playwright test (testDir ./e2e)

对 UI 与 API 之间的逻辑进行单元测试 — 查询键(通过 withStoreScope 按商店划分作用域)、表单 → payload 映射器、权限谓词、表格状态 → Ransack 参数。不要对组件或轻量 SDK 封装进行单元测试。每个功能编写一个 Playwright 规范,以证明各层能够连通:通过 Admin API 创建数据、登录、从侧边栏导航(路由按商店划分作用域),并断言可见文本。测试数据后缀加上 Date.now() — 端到端测试运行会共用一个数据库。详情:spree-dashboard-plugins。

扩展(gem)测试

扩展基于生成的虚拟应用进行测试:

bundle exec rake test_app      # regenerate spec/dummy after schema changes
bundle exec rspec

test_app / parallel_setup 仅存在于引擎和扩展中,应用中没有。

常见失败

现象原因 / 修复方法
Store must exist / Validation failed: Store must exist此上下文中没有默认商店 — 添加不带 without_global_store 的标签,或设置 Spree::Current.store / 传入 store:
工厂中出现 <Assoc> must exist必需的 belongs_to;构建关联或将其标记为 optional: true
variant 上出现 NoMethodError: price价格按货币区分 — 使用 price_in(currency) / amount_in(currency)
undefined method 'state' / state_machine没有状态机 — 断言 status(be_placed、status: 'canceled')
Order 工厂使用 state: 'complete'仍作为临时属性受支持,但建议使用 :completed_order_with_totals
货币错误Order/cart 工厂默认使用 USD;传入 currency:
规范中 Subscriber“从未运行”异步订阅者只会入队;直接调用 handle 或使用 perform_enqueued_jobs
OrderWalkthrough 弃用警告改用购物车工厂
时间测试不稳定使用 Timecop.freeze / travel_to

延伸阅读

  • 测试教程:node_modules/@spree/docs/dist/developer/tutorial/testing.md
  • 工厂:spree_core/lib/spree/testing_support/factories/
  • 共享上下文:spree_api/lib/spree/api/testing_support/v3/base.rb
  • spree_dev_tools:lib/spree_dev_tools/rspec/support/ 和安装生成器
  • 相关技能:spree-workflows、spree-events-webhooks、spree-resource、spree-auth-permissions、spree-dashboard-plugins

评论

支持 Markdown