О том, как не надо тестировать

Иван Стрелков

О том, как не надо тестировать

Иван Стрелков

Тестирование

О чем доклад?

Антипаттерны тестирования:
  1. Тестирование данных
  2. Тесты-близнецы
  3. Слишком сложный тест:
    • Верстка и сеть
    • Асинхронный код

Тестирование данных

Тестирование данных - Тест

      
describe('VideoView', () => {
  it('is editable', () => {
    expect(new VideoView().editable).toBe(true);
  });
});
      
    

Тестирование данных - Код

      
class VideoView extends GenericView {
  editable = true;

  // ...
}
      
    
Тестируются статические данные

Тестирование данных - Причины

Тестирование данных - Правильный тест

Тест-близнец

a.k.a. Mockery

Тест-близнец - Код


module.exports = {
  build() {
    firstLib.clean('build')
      .then(() => secondLib.compileJS('src', 'build'))
      .then(() => thirdLib.optimizeCSS())
  }
};
    

Тест-близнец - Код


module.exports = {
  build() {
    firstLib.clean('build')                               // очистка
      .then(() => secondLib.compileJS('src', 'build'))
      .then(() => thirdLib.optimizeCSS())
  }
};
    

Тест-близнец - Код


module.exports = {
  build() {
    firstLib.clean('build')
      .then(() => secondLib.compileJS('src', 'build'))    // JS
      .then(() => thirdLib.optimizeCSS())
  }
};
    

Тест-близнец - Код


module.exports = {
  build() {
    firstLib.clean('build')
      .then(() => secondLib.compileJS('src', 'build'))
      .then(() => thirdLib.optimizeCSS())                 // CSS
  }
};
    

Тест-близнец - Тест


describe('build step', () => {
  beforeEach(() => {
    // Подменяем библиотечные методы моками с resolved promises
    buildSystem.build('/dir1');
  });

  it('builds, compiles and does something else', () => {
    assert(firstLib.clean.calledWith('build'));

    assert(secondLib.compileJS.calledWith('src', 'build'));

    assert(thirdLib.optimizeCSS.called);
  });
});
    

Тест-близнец - Тест


describe('build step', () => {
  beforeEach(() => {
    // Подменяем библиотечные методы моками с resolved promises
    buildSystem.build('/dir1');
  });

  it('builds, compiles and does something else', () => {
    assert(firstLib.clean.calledWith('build'));             // очистка

    assert(secondLib.compileJS.calledWith('src', 'build'));

    assert(thirdLib.optimizeCSS.called);
  });
});
    

Тест-близнец - Тест


describe('build step', () => {
  beforeEach(() => {
    // Подменяем библиотечные методы моками с resolved promises
    buildSystem.build('/dir1');
  });

  it('builds, compiles and does something else', () => {
    assert(firstLib.clean.calledWith('build'));

    assert(secondLib.compileJS.calledWith('src', 'build')); // JS

    assert(thirdLib.optimizeCSS.called);
  });
});
    

Тест-близнец - Тест


describe('build step', () => {
  beforeEach(() => {
    // Подменяем библиотечные методы моками с resolved promises
    buildSystem.build('/dir1');
  });

  it('builds, compiles and does something else', () => {
    assert(firstLib.clean.calledWith('build'));

    assert(secondLib.compileJS.calledWith('src', 'build'));

    assert(thirdLib.optimizeCSS.called);                    // CSS
  });
});
    

Декларативный тест-близнец - Код


class HomePage extends React.Component {
  render() {
    return (
      <div>
        <Header userName={this.props.userName} />
        <Content> {this.props.invitation} </Content>
        <Footer showSocial />
      </div>
    )
  }
}
    

Декларативный тест-близнец - Тест


describe('HomePage', () => {
  beforeEach(() => { /* <HomePage userName="vasya" invitation="hello"/> */ });
  it('renders header with userName', () => {
    let header = TestUtils.findRenderedComponent(this.homePage, Header);
    expect(header.props.userName).toBe('vasya');
  });
  it('renders footer', () => {
    let footer = TestUtils.findRenderedComponent(this.homePage, Footer);
    expect(footer.props.showSocial).toBe(true);
  });
  it('renders invitation', () => {
    expect(this.container.innerHTML).toContain('hello');
  });
});
    

Декларативный тест-близнец - Тест


describe('HomePage', () => {
  beforeEach(() => { /* <HomePage userName="vasya" invitation="hello"/> */ });
  it('renders header with userName', () => {
    let header = TestUtils.findRenderedComponent(this.homePage, Header);
    expect(header.props.userName).toBe('vasya');
  });
  it('renders footer', () => {
    let footer = TestUtils.findRenderedComponent(this.homePage, Footer);
    expect(footer.props.showSocial).toBe(true);
  });
  it('renders invitation', () => {
    expect(this.container.innerHTML).toContain('hello');
  });
});
    

Декларативный тест-близнец - Тест


describe('HomePage', () => {
  beforeEach(() => { /* <HomePage userName="vasya" invitation="hello"/> */ });
  it('renders header with userName', () => {
    let header = TestUtils.findRenderedComponent(this.homePage, Header);
    expect(header.props.userName).toBe('vasya');
  });
  it('renders footer', () => {
    let footer = TestUtils.findRenderedComponent(this.homePage, Footer);
    expect(footer.props.showSocial).toBe(true);
  });
  it('renders invitation', () => {
    expect(this.container.innerHTML).toContain('hello');
  });
});
    

Декларативный тест-близнец - Тест


describe('HomePage', () => {
  beforeEach(() => { /* <HomePage userName="vasya" invitation="hello"/> */ });
  it('renders header with userName', () => {
    let header = TestUtils.findRenderedComponent(this.homePage, Header);
    expect(header.props.userName).toBe('vasya');
  });
  it('renders footer', () => {
    let footer = TestUtils.findRenderedComponent(this.homePage, Footer);
    expect(footer.props.showSocial).toBe(true);
  });
  it('renders invitation', () => {
    expect(this.container.innerHTML).toContain('hello');
  });
});
    

Тест-близнец - Причины

Тест-близнец - Правильный тест

Протестировать наличие в компоненте иконки Twitter

class HomePage extends React.Component {
  render() {
    return (
      <div>
        <Header userName={this.props.userName} />
        <Content children={this.props.invitation} />
        <Footer showSocial />
      </div>
    )
  }
}
    

Слишком
сложный
тест

Особенности тестирования фронтенда

Верстка и сеть

Верстка и сеть - Тест

Верстка и сеть - Правильный тест

Верстка и сеть - Page Object


test() {
    form.toggleTube();
    form.avitoBalloon().hide();
    form.toggleAvito();
    assert(form.avitoBalloon().visible === false);
}
    

Верстка и сеть - Советы

Асинхронный код

Асинхронный код - Код


class SearchForm extends React.Component {
  renderInput() {
    return <input ref="query" onChange={this.handleChange}/>;
  }
  @Debounce(200) @autobind
  handleChange() {
    this.setState({ state: 'loading' });
    fetchResults(this.refs.query.value).then(results => {
      this.setState({ state: 'loaded', results });
    });
  }
  // ...
}
    

Асинхронный код - Тест

Асинхронный код - Правильный код

      

class SearchForm extends React.Component {
  renderInput() {
    return <input ref="query" onChange={this.handleChange}/>;
  }
  @Debounce(200) @autobind
  handleChange() {
    this.setState({ state: 'loading' });
    fetchResults(this.refs.query.value).then(results => {
      this.setState({ state: 'loaded', results });
    });
  }
  // ...
}
    

Асинхронный код - Правильный код

      

class SearchForm extends React.Component {
  renderInput() {
    return <input ref="query" onChange={this.debouncedHandleChange}/>;
  }
   
  handleChange(value) {
    this.setState({ state: 'loading' });
    fetchResults(value).then(results => {
      this.setState({ state: 'loaded', results });
    });
  }
  @Debounce(200) @autobind
  debouncedHandleChange() {
    this.handleChange(this.refs.query.value);
  }
}
    

Асинхронный код - Правильный тест

Асинхронный код - Советы

Сухой остаток

Оправдывает ли тест свое написание?

Критерии оценки

Спасибо за внимание!

Иван Стрелков, Avito

Презентация: istrel.github.io