关于 spree-testing
spree-testing 是面向 Spree 应用和扩展开发者的测试指南,介绍 Rails、仪表板和浏览器测试工具,并提供工厂、工作流、API 测试及常见故障示例。
主要功能
- 01RSpec 与 Factory Bot 配置
- 02Spree 模型和结账工厂
- 03工作流、钩子与订阅者测试
- 04API v3 共享上下文和示例
- 05仪表板的 Vitest 与 Playwright 测试
- 06扩展的 Dummy 应用测试
适用场景
- 01为 Spree 应用添加模型和工作流测试
- 02测试 API v3 身份验证与权限
- 03用浏览器测试覆盖仪表板行为
- 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