# Stop White Box Testing Vue Components Use Testing Library Instead



## TL;DR

White box testing peeks into Vue internals, making your tests brittle. Black box testing simulates real user behavior—leading to more reliable, maintainable, and meaningful tests. Focus on behavior, not implementation.

## Introduction

Testing Vue components isn't about pleasing SonarQube or hitting 100% coverage; it's about having the confidence to refactor without fear, the confidence that your tests will catch bugs before users do.

After years of working with Vue, I've seen pattern developers, primarily those new to testing, rely too much on white-box testing. It inflates metrics but breaks easily and doesn't catch real issues.

Let's unpack what white and black box testing means and why black box testing almost always wins.

## What Is a Vue Component?

Think of a component as a function:

- **Inputs**: props, user events, external state
- **Outputs**: rendered DOM, emitted events, side effects

So, how do we test that function?

- Interact with the DOM and assert visible changes
- Observe side effects (store updates, emitted events)
- Simulate interactions like navigation or storage events

But here’s the catch _how_ you test determines the value of the test.

## White Box Testing: What It Is and Why It Fails

White box testing means interacting with internals: calling methods directly, reading `ref`s, or using `wrapper.vm`.

Example:

```ts
it("calls increment directly", () => {
  const wrapper = mount(Counter);
  const vm = wrapper.vm as any;

  expect(vm.count.value).toBe(0);
  vm.increment();
  expect(vm.count.value).toBe(1);
});
```

**Problems? Plenty:**

- **Brittle**: Refactor `increment` and this breaks—even if the UX doesn’t.
- **Unrealistic**: Users click buttons. They don’t call functions.
- **Misleading**: This test can pass even if the button in the UI does nothing.

## Black Box Testing: How Users Actually Interact

Black box testing ignores internals. You click buttons, type into inputs, and assert visible changes.

```js
it("increments when clicked", async () => {
  const wrapper = mount(Counter);

  expect(wrapper.text()).toContain("Count: 0");
  await wrapper.find("button").trigger("click");
  expect(wrapper.text()).toContain("Count: 1");
});
```

This test:

- **Survives refactoring**
- **Reflects real use**
- **Communicates intent**

## The Golden Rule: Behavior > Implementation

Ask: _Does the component behave correctly when used as intended?_

Good tests:

- ✅ Simulate real user behavior
- ✅ Assert user-facing outcomes
- ✅ Mock external dependencies (router, store, fetch)
- ❌ Avoid internal refs or method calls
- ❌ Don’t test implementation details

## Why Testing Library Wins

[Testing Library](https://testing-library.com/) enforces black box testing. It doesn’t even expose internals.

You:

- Find elements by role or text
- Click, type, tab—like a user would
- Assert what's visible on screen

Example:

```js
it("increments when clicked", async () => {
  const user = userEvent.setup();
  render(Counter);

  const button = screen.getByRole("button", { name: /increment/i });
  const count = screen.getByText(/count:/i);

  expect(count).toHaveTextContent("Count: 0");
  await user.click(button);
  expect(count).toHaveTextContent("Count: 1");
});
```

It’s readable, stable, and resilient.

### Bonus: Better Accessibility

Testing Library rewards semantic HTML and accessibility best practices:

- Proper labels and ARIA roles become _easier_ to test
- Icon-only buttons become harder to query (and rightly so)

```vue
<!-- ❌ Hard to test -->
<div class="btn" @click="increment">
  <i class="icon-plus"></i>
</div>

<!-- ✅ Easy to test and accessible -->
<button aria-label="Increment counter">
  <i class="icon-plus" aria-hidden="true"></i>
</button>
```

Win-win.

## Quick Comparison

|                         | White Box     | Black Box     |
| ----------------------- | ------------- | ------------- |
| Peeks at internals?     | ✅ Yes        | ❌ No         |
| Breaks on refactor?     | 🔥 Often      | 💪 Rarely     |
| Reflects user behavior? | ❌ Nope       | ✅ Yes        |
| Useful for real apps?   | ⚠️ Not really | ✅ Absolutely |
| Readability             | 🤯 Low        | ✨ High       |

## Extract Logic, Test It Separately

Black box testing doesn’t mean you can’t test logic in isolation. Just move it _out_ of your components.

For example:

```ts
// composable
export function useCalculator() {
  const total = ref(0);
  function add(a: number, b: number) {
    total.value = a + b;
    return total.value;
  }
  return { total, add };
}

// test
it("adds numbers", () => {
  const { total, add } = useCalculator();
  expect(add(2, 3)).toBe(5);
  expect(total.value).toBe(5);
});
```

Logic stays isolated, tests stay simple.

## Conclusion

- Treat components like black boxes
- Test user behavior, not code structure
- Let Testing Library guide your practice
- Extract logic to composables or utils

---

--- title: The Computed Inlining Refactoring Pattern in Vue description: Learn how to improve Vue component performance and readability by applying the Computed Inlining pattern - a technique inspired by Martin Fowler's Inline Function pattern. tags: ['vue', 'refactoring'] url: https://alexop.dev/posts/computed-inlining-refactoring-pattern-in-vue ---