Skip to content

Stop White Box Testing Vue Components Use Testing Library Instead

Published: at 

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:

So, how do we test that function?

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 refs, or using wrapper.vm.

Example:

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:

Black Box Testing: How Users Actually Interact

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

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:

The Golden Rule: Behavior > Implementation

Ask: Does the component behave correctly when used as intended?

Good tests:

Why Testing Library Wins

Testing Library enforces black box testing. It doesn’t even expose internals.

You:

Example:

import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'

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:

<!-- ❌ 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 BoxBlack 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:

// 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

Stay Updated!

Subscribe to my newsletter for more TypeScript, Vue, and web dev insights directly in your inbox.

  • Background information about the articles
  • Weekly Summary of all the interesting blog posts that I read
  • Small tips and trick
Subscribe Now

Related Posts