Test a role change by loading the site as a real user in that role, on a throwaway copy, after confirming the role definition in the database actually changed. The test most people run instead, edit the capability then refresh wp-admin as yourself, can pass while nothing was written at all.
That is the part the guides leave out. Roles in WordPress are not code that runs on each request. They are a row in wp_options, written once and then read forever.
Why a role is database state, not code
The Plugin Handbook is explicit about it: WordPress "stores the Roles and their Capabilities in the options table under the user_roles key" (Roles and Capabilities).
So add_role() is a write, not a declaration. Your PHP file describes what you wanted the first time it ran. It does not describe what is in the database now.
Every confusing role bug follows from that one fact.
The four things that make a role test lie
1. add_role() does nothing if the role already exists
The documented behaviour is that the function returns a WP_Role object on success, or null "if the role already exists" (add_role()). The Plugin Handbook puts it plainly: after the first call the role is stored, and "sequential calls will do nothing."
This is the single biggest source of false results. You add a capability to your add_role() array, save the file, reload, and the role is unchanged, because the role was already there and the call bailed out.
Nothing errors. Nothing logs. The code you are reading and the state you are testing have quietly diverged.
2. Deactivating the plugin does not undo the role
The common advice is to deactivate and reactivate so the activation hook fires again. That usually does not work either.
Deactivation only removes a role if the plugin explicitly calls remove_role() in its deactivation or uninstall routine, and most do not. So the role survives, and on reactivation add_role() hits the same no-op from point one.
You are now two layers away from a valid test, and the second layer looks like a fix.
3. You cannot test a role while you are an administrator
An administrator has the capability, so every check passes. Testing your Editor restrictions from your own admin session tells you nothing about the Editor.
On multisite it is worse. current_user_can() "will always return true if the current user is a super admin, unless specifically denied" (current_user_can()). A super admin passes checks for capabilities that exist in no role on the site.
If your site is multisite and you are the super admin, a role test run as yourself is structurally incapable of failing.
4. Meta capabilities are computed at request time
edit_post is not stored on a role. It is a meta capability that map_meta_cap() translates into primitive ones such as edit_posts and edit_others_posts, depending on the specific post being checked.
So a role can hold the right primitive capabilities and still deny the action, because ownership, post status, or a plugin filter changed the mapping for that object.
This is why "the capability is ticked in the role editor" is not evidence that the user can perform the action. Only performing the action is.
The routine that actually tests it
Do this on a disposable copy of the site, not on staging that shares a database or a users table with anything real.
Bring the site over with the Cloner plugin so the copy carries the same roles, the same plugins, and the same option values that any role editor already wrote. A clean install will not reproduce a role bug caused by two plugins that both edited wp_user_roles over three years.
Read the current state before you change anything. Over SSH with the CLI:
wp role list
wp cap list editor
wp user list --role=editor --fields=ID,user_login
wp role list shows what is in the database, which is the only authority. If a role appears here that no active plugin creates, that is point two, sitting in the options table.
Then apply your change and read it back. Not the code. The database:
wp cap list editor | grep manage_woocommerce
If the capability is not in that output, the change did not apply, and every click test you run after this point is measuring the old role.
Then log in as a real user in that role. Make a user on the copy, assign the role, and use a separate browser profile or a private window so you are not carrying your admin cookie.
Do not use a user-switching plugin for the final pass. It is a good tool for a quick look, but it changes the code path that establishes the session, and the thing you are testing is authorisation.
Then perform the actions, rather than reading the checkboxes. Open the edit screen. Save the post. Try to edit someone else's post. Upload a file. Visit /wp-admin/options-general.php directly by URL, because a hidden menu item is not a permission.
Turn on Debug mode first. A capability check that fails inside a plugin often surfaces as a notice in debug.log rather than as anything visible on screen.
Getting back to a known state
The reason this is worth doing on a sandbox is that role experiments are hard to reverse. wp_user_roles is a serialized array, and every plugin that has ever touched it left its edits in there.
WP-CLI can reset the built-in roles:
wp role reset --all
wp role delete broken_custom_role
That covers core's own roles. It does not restore a custom role to a state some plugin defined two versions ago.
Which is the real argument for a throwaway copy. Save the cloned site as a Template before you start, and every subsequent attempt begins from the same known state rather than from the wreckage of the last one. A sandbox also resets to a clean slate in one click if you would rather start over than unpick it.
Also check the user side, not just the role side. A user's role assignment lives in their own wp_capabilities usermeta:
wp user meta get 5 wp_capabilities
A user can be assigned a role that no longer exists. WordPress then gives them no capabilities at all, which looks like a login bug and is not one.
What a sandbox will not tell you
Be honest about the boundary here.
A copied site will not reproduce your host's object cache, so a capability that is cached per user in production may behave differently on the copy. It will not reproduce a WAF or an IP allowlist that blocks an admin path before WordPress sees the request.
It also will not reproduce SSO or an external identity provider that assigns roles at login. If your roles come from Okta, LDAP, or a membership platform keyed to your live domain, the mapping layer is the thing under test, and a sandbox tests only what happens after it.
And a cloned site carries the real user table. Treat it as production data: keep the sandbox private, keep its lifespan short, and do not hand the URL around. If the copy needs to be shared, scrub it first.
One more thing a sandbox is the wrong tool for. Do not test a role change by pushing the sandbox over production. Verify on the copy, then make the same change on the live site deliberately, with a backup.
The short version
The role in your code and the role in the database are two different things, and only one of them is being tested.
Read wp cap list before and after. Log in as a real user in that role, in a clean session. Perform the action instead of inspecting the checkbox.
Do all of it on a copy you can throw away, because the cheapest part of this job should be starting again. If you manage roles across a lot of client sites, agencies and support teams get most of the value here from the reset, not the test.
