Blog

  • Unit Testing WordPress Plugins in 2025 with @wordpress/env and PHPUnit

    Step 1: Set up @wordpress/env

    1.1 – Install the @wordpress/env package

    npm install @wordpress/env --save-dev

    Adding @wordpress/env as a dev dependency in your project ensures a consistent version and functionality is used across contributors.

    1.2 – Configure .wp-env.json

    Add a new file named .wp-env.json to the root of your project, with a basic configuration such as:

    {
        "core": "WordPress/WordPress",
        "plugins": [
            "."
        ],
        "themes": [],
    }

    See the documentation for additional configuration options.

    1.3 – Start it up!

    npm run wp-scripts start

    The initial startup will create the local Docker container where your environment runs.

    Step #2: Scaffolding Tests

    WP CLI provides a command to automatically set up tests in your project. The basic command looks like this:

    wp scaffold plugin-tests

    However, this must be run inside the local container. This can be done with the following, where “my-example-plugin” can be replaced with your plugin’s slug:

    npm run wp-env run cli --env-cwd=wp-content/plugins/my-example-plugin wp scaffold plugin-tests my-example-plugin

    Step #3: Install More Stuff

    The scaffolding step above will have inserted a bin/install-wp-tests.sh script, which we should run:

    npm run wp-env run cli --env-cwd=wp-content/plugins/my-example-plugin bash bin/install-wp-tests.sh wordpress_test root password mysql

    Step #4: Configure PHPUnit

    4.1 – Add phpunit.xml

    The scaffolding step will also have added a phpunit.xml.dist file to the root of your project.

    Create a new file name phpunit.xml and copy the contents of the .dist file into it.

    4.2 – Require @yoast/phpunit-polyfills

    composer required @yoast/phpunit-polyfills@1.0.1

    Step #5: Run The Tests

    You can now run your project’s tests via:

    npm run wp-env run cli --env-cwd=wp-content/plugins/my-example-plugin vendor/bin/phpunit

    Hooray!

  • Locality of Behavior (LoB) in Modern Frontend Architecture

    “The primary feature for easy maintenance is locality: Locality is that characteristic of source code that enables a programmer to understand that source by looking at only a small portion of it.

    Richard Gabriel
    Carson Gross
    – Nate Weller

    What is Locality of Behavior?

    In short: The behavior of a unit of code should be as obvious as possible by looking only at that unit of code.

    This sounds deceptively simple, but it’s violated constantly in modern frontend codebases. As applications grow in complexity, the distance between a component’s appearance and its actual behavior tends to increase.

    The Tale of Two Buttons

    Consider these two implementations of a button that makes an AJAX request:

    Example 1 (htmx):

    <button hx-get="/clicked">Click Me</button>

    Example 2 (JS):

    <button id="d1">Click Me</button>
    document.getElementById('d1').addEventListener('click', () => {
      fetch('/clicked', {
        method: 'GET',
      })
      .then(response => response.json())
      .then(data => {
        // Handle response data
      });
    });

    In the first example, the button’s behavior is immediately obvious when looking at the element itself. In the second, understanding what happens when clicking requires finding the corresponding JavaScript that might live anywhere in your application.

    This distinction might seem minor for a small application, but as your codebase grows to thousands of components, the ability to quickly understand behavior becomes critical.

    LoB in Component Architecture

    Modern frontend frameworks have increasingly embraced component-based architectures, which provide natural boundaries for implementing locality. A well-designed React, Vue, or Angular component encapsulates everything needed to understand its behavior:

    • Template/HTML structure
    • Logic/JavaScript behavior
    • Styling/CSS presentation
    • Tests for verification

    Co-locating these elements together brings benefits:

    1. Maintainability: Changes to one aspect of a component don’t require hunting through multiple files
    2. Discoverability: New team members can understand a component by examining a single location
    3. Reduced cognitive load: Developers can hold the complete behavior in mind without context switching

    I’ve found the most maintainable components are those where I can understand their complete behavior by examining a single file or directory, without needing to trace behavior through indirect connections across the codebase.

    State Management Through the LoB Lens

    Locality is likewise highly applicable to state management. State that affects a component’s behavior should ideally live as close as possible to that component.

    Early React emphasized lifting state upward, but this often created situations where changes in one component would unexpectedly affect another. The pendulum has swung back toward local state with hooks, which make it easy to keep behavior tightly bound to components.

    For example, compare these approaches:

    Distant state:

    // In a global store file
    const globalState = {
      userPreferences: {
        theme: 'dark',
        fontSize: 'medium'
      }
    };
    
    // In a component file far, far away
    function UserCard() {
      // Where does this come from? What else can change it?
      const { theme } = useGlobalState();
      
      return <div className={`card ${theme}`}>...</div>;
    }

    Local state with clear escalation paths:

    function UserCard({ theme }) {
      // State needed only by this component stays here
      const [isExpanded, setIsExpanded] = useState(false);
      
      return (
        <div className={`card ${theme}`}>
          <button onClick={() => setIsExpanded(!isExpanded)}>
            {isExpanded ? 'Show Less' : 'Show More'}
          </button>
          {isExpanded && <div>Extra content here</div>}
        </div>
      );
    }

    The second approach makes behavior immediately obvious by keeping state close to where it’s used, while still allowing shared state to be passed explicitly.

    Common Anti-patterns and Solutions

    1. Event Bus Abuse: Using a global event bus where components emit and listen for events without clear connections
    2. CSS Class Treasure Hunts: Component appearance determined by distant CSS selectors
    3. Utility File Sprawl: Extracting any potentially reusable code into utility files, creating a maze of dependencies
    4. Prop Drilling: Passing props through multiple component layers, obscuring the data flow

    The solution usually involves restructuring to bring behavior closer to the components affected:

    • Replace event buses with explicit props and callbacks
    • Use CSS-in-JS or scoped CSS to keep styles with components
    • Keep utility functions with the components that use them until there’s clear evidence of broader reuse
    • Use composition to avoid excessive prop drilling

    Balancing LoB with Other Principles

    LoB will inevitably conflict with other software principles. Two important tensions to manage:

    LoB vs. DRY (Don’t Repeat Yourself)

    Absolute adherence to DRY can lead to premature abstraction, pulling related behavior apart. I’ve learned to tolerate strategic duplication when it improves locality. As the saying goes: “Duplication is far cheaper than the wrong abstraction.”

    LoB vs. SoC (Separation of Concerns)

    Traditional Separation of Concerns organizes code by technical type (HTML, CSS, JS), but component architecture reorganizes around functional concerns instead. This is why inline styles and CSS-in-JS have gained popularity despite initially seeming to violate SoC. They improve locality by keeping related concerns together.

    The key is finding the right level of abstraction where components remain coherent, self-contained units without becoming monolithic.

    Practical Implementation Strategies

    Here are strategies I’ve found effective for improving locality in frontend codebases:

    1. Default to co-location: Start with everything related to a component in one file or directory. Extract only when complexity demands it.
    2. Use composition over configuration: Make component behavior explicit through composition rather than through complex configuration objects.
    3. Implement “Screaming Architecture”: Name and organize files so their purpose is immediately obvious, not by technical type but by domain feature.
    4. Document the unexpected: When behavior can’t be made obvious through code structure, document it explicitly where the component is defined.
    5. Make state transitions visible: Use state machines or explicit state transitions to make behavior changes obvious.

    Real-world Benefits

    Teams I’ve led that embraced locality have seen significant improvements:

    • Faster onboarding: New developers could become productive within days rather than weeks
    • Reduced “tribal knowledge”: Less reliance on “ask Sarah, she wrote that component”
    • More confident refactoring: Changes could be made with greater confidence that unexpected side effects wouldn’t emerge
    • Easier code reviews: Reviewers could understand changes without extensive context

    Conclusion

    Locality of Behavior isn’t just another software principle—it’s about creating humane systems that respect cognitive limitations. By keeping related code together and making behavior obvious, we build codebases that remain maintainable as they grow.