テスト戦略
Shopify アプリは複雑な環境で動作します -- 埋め込み iframe、OAuth フロー、Webhook 処理、GraphQL API、ストアフロントレンダリング。堅牢なテスト戦略はすべてのレイヤーをカバーし、アプリがマーチャントシナリオの全範囲にわたって正しく動作するという確信を与えます。このモジュールでは、ユニットテスト、統合テスト、エンドツーエンドテスト、Shopify テストエコシステムの最新ツールについて説明します。
Vitest によるユニットテスト
Vitest は Remix で構築された最新の Shopify アプリに推奨されるテストランナーです。高速で、ネイティブ ESM サポートがあり、Shopify アプリテンプレートとクリーンに統合されます。
Vitest のセットアップ
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
// vitest.config.js
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./test/setup.js'],
include: ['**/*.test.{js,jsx,ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
exclude: [
'node_modules/',
'test/',
'**/*.config.*',
'**/types/**',
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
resolve: {
alias: {
'~': path.resolve(__dirname, 'app'),
},
},
});
// test/setup.js
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
afterEach(() => {
cleanup();
});
// Mock Shopify App Bridge globally
vi.mock('@shopify/app-bridge-react', () => ({
useAppBridge: () => ({
dispatch: vi.fn(),
subscribe: vi.fn(),
getState: vi.fn(),
}),
TitleBar: ({ title }) => <div data-testid="title-bar">{title}</div>,
useNavigate: () => vi.fn(),
}));
ビジネスロジックのテスト
// services/pricing-calculator.test.js
import { describe, it, expect } from 'vitest';
import { calculateDiscount, applyVolumePrice } from './pricing-calculator';
describe('PricingCalculator', () => {
describe('calculateDiscount', () => {
it('applies percentage discount correctly', () => {
const result = calculateDiscount({
originalPrice: 100.0,
discountType: 'percentage',
discountValue: 15,
});
expect(result).toEqual({
discountedPrice: 85.0,
savings: 15.0,
discountApplied: true,
});
});
it('does not allow negative prices', () => {
const result = calculateDiscount({
originalPrice: 10.0,
discountType: 'fixed',
discountValue: 20.0,
});
expect(result.discountedPrice).toBe(0);
expect(result.savings).toBe(10.0);
});
it('handles zero-price items gracefully', () => {
const result = calculateDiscount({
originalPrice: 0,
discountType: 'percentage',
discountValue: 50,
});
expect(result.discountedPrice).toBe(0);
expect(result.discountApplied).toBe(false);
});
it('rounds to two decimal places for currency precision', () => {
const result = calculateDiscount({
originalPrice: 19.99,
discountType: 'percentage',
discountValue: 33.33,
});
expect(result.discountedPrice).toBe(13.33);
});
});
describe('applyVolumePrice', () => {
const tiers = [
{ minQuantity: 1, maxQuantity: 9, pricePerUnit: 10.0 },
{ minQuantity: 10, maxQuantity: 49, pricePerUnit: 8.5 },
{ minQuantity: 50, maxQuantity: Infinity, pricePerUnit: 7.0 },
];
it('selects the correct tier for quantity', () => {
expect(applyVolumePrice(5, tiers)).toBe(10.0);
expect(applyVolumePrice(25, tiers)).toBe(8.5);
expect(applyVolumePrice(100, tiers)).toBe(7.0);
});
it('uses the boundary quantity correctly', () => {
expect(applyVolumePrice(10, tiers)).toBe(8.5);
expect(applyVolumePrice(50, tiers)).toBe(7.0);
});
});
});
Polaris を使った React コンポーネントのテスト
// components/ProductCard.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { PolarisTestProvider } from '@shopify/polaris';
import enTranslations from '@shopify/polaris/locales/en.json';
import { ProductCard } from './ProductCard';
function renderWithPolaris(component) {
return render(
<PolarisTestProvider i18n={enTranslations}>
{component}
</PolarisTestProvider>
);
}
describe('ProductCard', () => {
const mockProduct = {
id: 'gid://shopify/Product/123',
title: 'Test Widget',
status: 'ACTIVE',
totalInventory: 42,
featuredImage: { url: 'https://cdn.shopify.com/test.jpg' },
};
it('renders product information correctly', () => {
renderWithPolaris(<ProductCard product={mockProduct} />);
expect(screen.getByText('Test Widget')).toBeInTheDocument();
expect(screen.getByText('42 in stock')).toBeInTheDocument();
});
it('shows warning badge for low inventory', () => {
const lowStockProduct = { ...mockProduct, totalInventory: 3 };
renderWithPolaris(<ProductCard product={lowStockProduct} />);
expect(screen.getByText('Low stock')).toBeInTheDocument();
});
it('calls onSync when sync button is clicked', async () => {
const onSync = vi.fn().mockResolvedValue({ success: true });
renderWithPolaris(<ProductCard product={mockProduct} onSync={onSync} />);
fireEvent.click(screen.getByRole('button', { name: /sync/i }));
await waitFor(() => {
expect(onSync).toHaveBeenCalledWith('gid://shopify/Product/123');
});
});
});
Webhook ハンドラーのテスト
// webhooks/orders-paid.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest';
import crypto from 'crypto';
import { handleOrderPaid } from './orders-paid';
describe('Orders Paid Webhook', () => {
let mockReq, mockRes;
beforeEach(() => {
mockRes = {
status: vi.fn().mockReturnThis(),
send: vi.fn(),
};
});
function createSignedRequest(body) {
const rawBody = JSON.stringify(body);
const hmac = crypto
.createHmac('sha256', process.env.SHOPIFY_API_SECRET || 'test-secret')
.update(rawBody, 'utf8')
.digest('base64');
return {
headers: { 'x-shopify-hmac-sha256': hmac },
rawBody,
body,
get: (header) => hmac,
};
}
it('processes a valid paid order', async () => {
const orderPayload = {
id: 456,
total_price: '99.99',
line_items: [{ product_id: 789, quantity: 2 }],
};
mockReq = createSignedRequest(orderPayload);
await handleOrderPaid(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
});
it('rejects requests with invalid HMAC', async () => {
mockReq = {
headers: { 'x-shopify-hmac-sha256': 'invalid-hmac' },
rawBody: '{}',
body: {},
get: () => 'invalid-hmac',
};
await handleOrderPaid(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(401);
});
});
Shopify CLI による統合テスト
Shopify CLI provides tools for testing your app against real Shopify stores in a development context.
# Start your app in development mode with a test store
shopify app dev --store=your-test-store.myshopify.com
# Run your integration tests against the running dev server
SHOPIFY_TEST_STORE=your-test-store.myshopify.com npm run test:integration
Shopify に対する GraphQL クエリのテスト
// test/integration/products-api.test.js
import { describe, it, expect, beforeAll } from 'vitest';
import { createTestClient } from '../helpers/shopify-test-client';
describe('Products API Integration', () => {
let client;
beforeAll(async () => {
client = await createTestClient();
});
it('fetches products with correct fields', async () => {
const response = await client.query({
data: {
query: `{
products(first: 5) {
edges {
node {
id
title
status
totalInventory
}
}
}
}`,
},
});
const products = response.body.data.products.edges;
expect(products.length).toBeGreaterThan(0);
expect(products[0].node).toHaveProperty('id');
expect(products[0].node).toHaveProperty('title');
});
});
Integration tests that hit real Shopify APIs consume API rate limits. Run them sparingly in CI -- typically only on pull requests targeting main, not on every push. Use mocked responses for unit tests and reserve live API calls for integration suites.
エンドツーエンドテスト
E2E tests verify the complete user journey from a merchant installing your app to performing key actions.
Shopify アプリ向け Playwright セットアップ
// playwright.config.js
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './test/e2e',
timeout: 60000, // Shopify admin can be slow
retries: 2,
use: {
baseURL: process.env.APP_URL || 'http://localhost:3000',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
});
// test/e2e/app-dashboard.spec.js
import { test, expect } from '@playwright/test';
test.describe('App Dashboard', () => {
test.beforeEach(async ({ page }) => {
// Navigate to the app within Shopify admin
await page.goto('/app');
// Wait for App Bridge to initialize
await page.waitForSelector('[data-testid="app-loaded"]');
});
test('displays product sync status', async ({ page }) => {
await page.click('text=Product Sync');
await expect(page.locator('[data-testid="sync-status"]')).toBeVisible();
await expect(page.locator('[data-testid="last-sync-time"]')).not.toBeEmpty();
});
test('can trigger manual sync', async ({ page }) => {
await page.click('text=Product Sync');
await page.click('button:has-text("Sync Now")');
// Wait for sync to complete
await expect(page.locator('[data-testid="sync-progress"]')).toBeVisible();
await expect(page.locator('text=Sync complete')).toBeVisible({
timeout: 30000,
});
});
});
ストアフロントテスト用 SimGym
SimGym is Shopify's storefront simulation environment introduced in Winter '26. It allows you to test your app's storefront impact -- theme app extensions, script tags, and dynamic content -- without affecting a live store. SimGym spins up an isolated storefront with synthetic traffic patterns, enabling you to measure performance impact before deployment.
# Install SimGym CLI extension
shopify plugins install @shopify/simgym
# Run a storefront simulation with your app's theme extension
shopify simgym run \
--theme-extension=./extensions/theme-block \
--traffic-profile=holiday-sale \
--duration=300 \
--report=./simgym-report.html
SimGym generates a detailed report including Web Vitals impact, render timing, and JavaScript execution costs. Use it as a gate in your CI pipeline:
# .github/workflows/storefront-perf.yml
- name: SimGym Performance Test
run: |
shopify simgym run \
--theme-extension=./extensions/theme-block \
--traffic-profile=standard \
--assert-lcp-under=2500 \
--assert-cls-under=0.1 \
--assert-inp-under=200
A/B テスト用 Rollouts(Winter '26)
Winter '26 introduces Rollouts, Shopify's built-in A/B testing framework for apps. Rollouts lets you progressively deploy new features to a percentage of merchants and measure the impact on key metrics before full rollout.
// services/rollouts.js
import { shopifyApi } from '@shopify/shopify-api';
export async function checkFeatureFlag(session, featureName) {
const client = new shopify.clients.Graphql({ session });
const response = await client.query({
data: {
query: `query CheckRollout($feature: String!) {
appRollout(feature: $feature) {
enabled
variant
metadata
}
}`,
variables: { feature: featureName },
},
});
return response.body.data.appRollout;
}
// Usage in a route loader
export async function loader({ request }) {
const session = await getSession(request);
const newDashboard = await checkFeatureFlag(session, 'new-dashboard-v2');
return json({
useNewDashboard: newDashboard.enabled,
dashboardVariant: newDashboard.variant, // 'control' or 'treatment'
});
}
高トラフィックイベントのロードテスト
Shopify merchants experience massive traffic spikes during flash sales, BFCM (Black Friday / Cyber Monday), and product drops. Your app must handle these gracefully.
k6 によるロードテスト
// test/load/webhook-throughput.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const failureRate = new Rate('failed_requests');
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp up
{ duration: '5m', target: 200 }, // Sustained peak (simulates BFCM)
{ duration: '2m', target: 500 }, // Spike (flash sale start)
{ duration: '1m', target: 50 }, // Cool down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
failed_requests: ['rate<0.01'], // Less than 1% failure rate
},
};
export default function () {
const payload = JSON.stringify({
id: Math.floor(Math.random() * 1000000),
total_price: '59.99',
line_items: [{ product_id: 123, quantity: 1 }],
});
const res = http.post(`${__ENV.APP_URL}/webhooks/orders-create`, payload, {
headers: {
'Content-Type': 'application/json',
'X-Shopify-Hmac-Sha256': 'test-hmac', // Use valid HMAC in staging
'X-Shopify-Shop-Domain': 'load-test.myshopify.com',
},
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
failureRate.add(res.status !== 200);
sleep(0.1);
}
# Run the load test
k6 run --env APP_URL=https://staging.your-app.com test/load/webhook-throughput.js
Never run load tests against Shopify's production APIs or a live merchant store. Load test only your own infrastructure. Use mock Shopify responses in your staging environment to simulate realistic API behavior under load.
テスト戦略まとめ
| レイヤー | ツール | テスト内容 | 頻度 |
|---|---|---|---|
| Unit | Vitest | Business logic, components, utils | Every commit |
| Integration | Vitest + Shopify CLI | API interactions, data flows | Every PR |
| E2E | Playwright | Full user journeys | Pre-release |
| Storefront | SimGym | Performance impact on storefront | Pre-release |
| A/B | Rollouts | Feature effectiveness | Post-release |
| Load | k6 | Throughput and latency under stress | Pre-BFCM, quarterly |
十分にテストされた Shopify アプリはマーチャントの信頼を獲得し、初回でアプリレビューに合格し、高トラフィックイベントをインシデントなく乗り越えます。テストインフラストラクチャに早期に投資しましょう -- サポート負担の軽減とイテレーション速度の向上という形で配当を得られます。