Shopify has spent years building one of the most extensible commerce platforms available. You can customize the checkout. You can inject logic into Shopify’s pricing and discount engine via Functions. You can build UI components that render natively inside the admin. You can run a full custom app that talks to every corner of the platform.
What Shopify didn’t build, to anywhere near the same degree, is an automated testing toolchain.
With Shopify moving ever more aggressively into the enterprise segment, that gap matters more than it used to. A complex Shopify build today may span any of the following:
- Shopify theme: Liquid templates, section schemas, and increasingly complex rendering logic that runs in Shopify’s CDN and is never in your hands at runtime.
- Shopify Functions: business logic that compiles to WebAssembly and runs inside Shopify’s infrastructure (pricing rules, discount logic, payment method filtering).
- UI Extensions: React components that render inside the checkout, the order status page, or the Shopify admin, inside an iframe you don’t control.
- Custom app(s): good old web applications that may receive and respond to webhooks from Shopify, render Liquid or Admin UI via the App Proxy, and anything else that doesn’t fit in Shopify’s existing extension points.
For each of these layers, automated testing requires different tools, poses different constraints, and calls for different workarounds. Shopify offers some testing solutions in their documentation. Others, we’ve had to figure out ourselves.
Let’s take a look at the state of the art, shall we?
Testing Shopify Themes
Shopify has no official testing framework for themes. No test runner, no test helpers, no fixture system.
Our main approach is to run Playwright against a preview theme. The workflow: when a PR is opened, we generate a preview theme from the live theme, run Playwright against it (accessibility and performance checks, end-to-end purchase flows, and anything else we need), and require all the checks to pass before the PR can be merged.
It works, with a couple of constraints:
- The main constraint is that you can’t alter the Shopify store’s state before running the tests, as that would alter your production configuration. For example, if you need to verify behavior that depends on a specific product configuration, that product needs to already exist in the store when the test runs. If it doesn’t, you can’t test the scenario.
- You cannot easily test the Shopify checkout, as that would require placing a real order with a real credit card. For some clients, we have done this with virtual cards and a Shopify Flow that auto-cancels any test orders coming from specific email addresses, but it’s a cumbersome solution.
A possible solution to both problems is to run the tests against your staging store rather than the production one. That comes with its own issues, which we walked through in our video on staging-production parity.
For snippets with significant branching logic, Playwright wasn’t enough. So we built minitest_shopify_themes, an experimental library that renders Shopify theme snippets and sections in isolation using Minitest and Capybara. You pass in data fixtures directly rather than depending on store state, which means you can test dozens of combinations quickly and deterministically. Check it out!
We’re calling it an experiment because that’s what it is: it works well for what it covers, but dynamic blocks and inter-snippet rendering don’t behave correctly in a test environment, and since Shopify doesn’t officially support this approach, there’s no guarantee it stays viable as the platform evolves. We use it selectively: Playwright as the base, minitest_shopify_themes for snippets where the logic branches enough that combinatorial coverage is worth the investment.
Testing Shopify Functions
For Shopify Functions, testing is a much gentler beast to tame.
Much of it is a result of the underlying architecture. Shopify Functions compile to WebAssembly and run as pure functions: they receive a GraphQL input, produce a GraphQL output, and have no access to network or external state (unless you’re one of the very few Shopify Enterprise customers with network access). Because of this, there’s nothing to mock. The whole function is its own test fixture.
This means you can import a Function directly into a test file and call it like any other JavaScript function. Write your input, assert against the output. We run these tests across all extensions from a single npm run test command at the project root, and they gate deployments in CI: if a Function’s tests fail, the build doesn’t ship.
Furthermore, Shopify provides a type generation tool (typegen) that generates TypeScript types from your GraphQL query. The practical effect is autocomplete and compile-time checking against Shopify’s actual schema, which catches structural mismatches before they reach production.
The one limit to keep in mind is that this approach doesn’t tell you whether the Function will behave correctly against your real Shopify data in your real Shopify store. For testing Shopify Functions in integration, you can either use Playwright or exercise your store’s Storefront API.
Testing Shopify UI Extensions
UI extensions that alter the checkout, customer account, or admin panel all run in Shopify-served iframes. You can’t control the context in which they run, and Shopify does not provide an official testing path for these either. So we are back to building our own, though the shape of the answer differs from themes.
Playwright covers the happy path. Where extension logic branches, we mock Shopify’s extension library APIs at the module level, using global mocks that let extension code run in isolation from the Shopify host. That is what buys us coverage on the conditional rendering Playwright can’t reach without a real order sitting in a real checkout.
The risk is that the mocks rot. Shopify changes an API, our mock keeps returning the old shape, and the suite goes green against behavior that no longer exists. That is worse than having no test at all: a failing test gets fixed, while a test that passes for the wrong reason ships a broken extension and tells you the build is fine.
Ultimately, there’s no good answer here. You’re picking which way you’d rather be wrong: fewer tests and thinner coverage, or more tests and the chance that some of them are lying to you.
We pick the second and keep Playwright around to catch the lies. Smoke tests run against the real host, so they’re the only thing that spots the drift. Unit tests sit on top and buy coverage on the branches. Drop the smoke tests and you’re trusting your own fiction about how Shopify behaves.
Testing Custom Shopify Apps
Because they sit outside the Shopify runtime, custom apps are typically the easiest part to test: just use whatever test tooling is offered by your language or framework of choice. For us, that’s usually RSpec for Ruby on Rails or Vitest for Next.js and Nest.js.
That said, a couple of caveats still apply.
The first one has to do with webhook versioning. Shopify webhooks carry the payload structure from the API version configured in your app, and your test fixtures are written to mirror that structure. When you upgrade API versions, which Shopify requires as it deprecates older ones, your mocked payloads can silently diverge from what Shopify now sends. The tests keep passing against payloads that no longer reflect reality, right up until a live order arrives and the app chokes on a field that moved. Catching this takes deliberate attention at every version bump: pull up the webhook fixtures, check them against the new version’s schema, and update anything that changed. Ask us how we know.
The second caveat is about App Bridge, Shopify’s JavaScript SDK for building native admin UI inside your app. App Bridge relies on the Shopify admin’s iframe context to authenticate and render, and that context cannot be reproduced in a test environment. The most reliable approach we’ve found is to move all business logic out of the App Bridge components and test that logic on its own. App Bridge then becomes a thin presentation layer, and the code worth testing lives underneath it. If logic and presentation are already tangled together inside your App Bridge components, pulling them apart is expensive, but there is no other route to decent coverage.
The Integration Layer That Doesn’t Exist
Unfortunately, there’s a harder problem underneath all of this, and no tool currently solves it.
Each layer can be tested, in varying degrees, with the approaches above. What can’t be tested reliably is how all the layers behave together. A Function modifies cart line items. A checkout extension reads those line items to determine what to display. The theme renders an element that reflects the extension’s output. The app records the resulting order event. Test each layer in isolation and all four pass. Whether they compose correctly under real conditions, with real data, in a real checkout… well, that’s a different question.
This is the classic problem of distributed systems testing: individual components can be correct while the system as a whole is still wrong. Shopify’s ecosystem is vast and compositional by design. That’s the point of it. But composability and testability pull in opposite directions, and in a stack this distributed, full integration testing is slow, fragile, and expensive to maintain, and often prohibitively so.
How do you solve this problem, you ask? Well, you don’t. You just manage it.
What we’ve found helps:
- Monorepos for your code, so that changes across layers can be merged and deployed atomically.
- Contracts between layers, written down: what does this Function guarantee about the shape of its output?
- Change management discipline, so that a change in one layer reaches whoever owns the layers downstream of it.
- An end-to-end smoke suite covering the highest-stakes paths: add to cart, checkout, order confirmation.
What Good Looks Like on a Shopify Stack
If you come from a more structured software engineering or digital product background, this is the part that stings: full coverage of a Shopify stack simply isn’t on the menu. That’s not a failure of your team. It’s a structural property of a platform where much of your code runs on someone else’s infrastructure, in a context you can’t reproduce.
So the goal moves. You aren’t chasing a coverage number anymore. You’re deciding, deliberately, where you’re exposed and whether you can live with it.
On the teams where this works well, three things tend to be true. Everyone can say how well each layer is covered and where the thin spots are, so nobody reads a green build as a safe deploy. The code is written so it can be exercised in isolation, with business logic kept away from rendering and rendering kept away from Shopify. And somebody has gone to the trouble of mocking Shopify’s primitives for the parts that look untestable, which, with AI writing most of the mock, is a far cheaper thing to attempt than it was two years ago.
None of this gets you to full coverage. It gets you to knowing exactly which parts of your store you are shipping on faith.
And if this is the kind of problem your team is working through, we’d be glad to talk about it.




