Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
145 lines
4.6 KiB
Markdown
145 lines
4.6 KiB
Markdown
---
|
|
title: Test Components Using Blackbox Approach - Focus on Behavior Not Implementation
|
|
impact: HIGH
|
|
impactDescription: Implementation-aware tests become brittle and break during refactoring, leading to high maintenance burden
|
|
type: best-practice
|
|
tags: [vue3, testing, component-testing, vitest, vue-test-utils, blackbox]
|
|
---
|
|
|
|
# Test Components Using Blackbox Approach - Focus on Behavior Not Implementation
|
|
|
|
**Impact: HIGH** - Tests that rely on implementation details (internal state, private methods, component structure) break during refactoring even when functionality remains correct. This leads to false negatives and high test maintenance burden.
|
|
|
|
Follow Kent C. Dodds' testing philosophy: "The more your tests resemble how your software is used, the more confidence they can give you."
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Test what the component does, not how it does it
|
|
- [ ] Query elements by user-visible attributes (text, role, testid)
|
|
- [ ] Simulate user interactions (click, type) rather than calling methods directly
|
|
- [ ] Assert on rendered output, emitted events, and visible state changes
|
|
- [ ] Avoid accessing component internal state or private methods
|
|
- [ ] Use data-testid attributes for elements without semantic meaning
|
|
|
|
**Incorrect:**
|
|
```javascript
|
|
import { mount } from '@vue/test-utils'
|
|
import Counter from './Counter.vue'
|
|
|
|
// BAD: Testing implementation details
|
|
test('counter increments', async () => {
|
|
const wrapper = mount(Counter)
|
|
|
|
// Accessing internal state directly
|
|
expect(wrapper.vm.count).toBe(0)
|
|
|
|
// Calling internal method instead of simulating user action
|
|
wrapper.vm.increment()
|
|
|
|
// Checking internal state instead of visible output
|
|
expect(wrapper.vm.count).toBe(1)
|
|
})
|
|
|
|
// BAD: Testing component structure
|
|
test('has increment button', () => {
|
|
const wrapper = mount(Counter)
|
|
|
|
// Testing implementation detail - what if button becomes an anchor?
|
|
expect(wrapper.find('button').exists()).toBe(true)
|
|
})
|
|
```
|
|
|
|
**Correct:**
|
|
```javascript
|
|
import { mount } from '@vue/test-utils'
|
|
import Counter from './Counter.vue'
|
|
|
|
// CORRECT: Testing behavior like a user would
|
|
test('counter displays updated value after clicking increment', async () => {
|
|
const wrapper = mount(Counter, {
|
|
props: { max: 10 }
|
|
})
|
|
|
|
// Assert initial visible state
|
|
expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('0')
|
|
|
|
// Simulate user action
|
|
await wrapper.find('[data-testid="increment-button"]').trigger('click')
|
|
|
|
// Assert visible result
|
|
expect(wrapper.find('[data-testid="counter-value"]').text()).toContain('1')
|
|
})
|
|
|
|
// CORRECT: Testing emitted events (public API)
|
|
test('emits change event with new value when incremented', async () => {
|
|
const wrapper = mount(Counter)
|
|
|
|
await wrapper.find('[data-testid="increment-button"]').trigger('click')
|
|
|
|
expect(wrapper.emitted('change')).toHaveLength(1)
|
|
expect(wrapper.emitted('change')[0]).toEqual([1])
|
|
})
|
|
```
|
|
|
|
## Using @testing-library/vue for Better Blackbox Tests
|
|
|
|
```javascript
|
|
import { render, screen, fireEvent } from '@testing-library/vue'
|
|
import Counter from './Counter.vue'
|
|
|
|
// Testing Library encourages accessible, user-centric queries
|
|
test('increments counter on button click', async () => {
|
|
render(Counter)
|
|
|
|
// Query by role - how screen readers see it
|
|
const button = screen.getByRole('button', { name: /increment/i })
|
|
const display = screen.getByText('0')
|
|
|
|
await fireEvent.click(button)
|
|
|
|
expect(screen.getByText('1')).toBeInTheDocument()
|
|
})
|
|
```
|
|
|
|
## What to Test vs What Not to Test
|
|
|
|
### DO Test (Public Interface)
|
|
```javascript
|
|
// Props affect rendered output
|
|
test('shows title from props', () => {
|
|
const wrapper = mount(Card, {
|
|
props: { title: 'Hello World' }
|
|
})
|
|
expect(wrapper.text()).toContain('Hello World')
|
|
})
|
|
|
|
// Slots render correctly
|
|
test('renders slot content', () => {
|
|
const wrapper = mount(Card, {
|
|
slots: { default: '<p>Slot content</p>' }
|
|
})
|
|
expect(wrapper.text()).toContain('Slot content')
|
|
})
|
|
|
|
// Emitted events
|
|
test('emits close event when X clicked', async () => {
|
|
const wrapper = mount(Modal)
|
|
await wrapper.find('[data-testid="close-button"]').trigger('click')
|
|
expect(wrapper.emitted('close')).toBeTruthy()
|
|
})
|
|
```
|
|
|
|
### DON'T Test (Implementation Details)
|
|
```javascript
|
|
// Don't test internal computed properties
|
|
// Don't test internal methods
|
|
// Don't test component options/setup internals
|
|
// Don't test that specific child components are rendered (unless critical)
|
|
// Don't rely exclusively on snapshot tests for correctness
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js Testing Guide](https://vuejs.org/guide/scaling-up/testing)
|
|
- [Vue Test Utils - Testing Philosophy](https://test-utils.vuejs.org/guide/)
|
|
- [Testing Library Guiding Principles](https://testing-library.com/docs/guiding-principles)
|