Как стать автором
Обновить

Комментарии 20

Итак, взяли смешной детский пример, бесконечно далёкий от реальности, написали 29 строчек тривиальнейшего кода, и 50 строчек тестов. Наверное, это хорошо. Непонятно только, почему.

И, разумеется, все тесты нафиг поломаются, если мы заменим одну либу модальных окон на другую с таким же API. Всё равно поломаются.

Что конкретно призвана иллюстрировать статья? Что на простейший для понимания код легко и непринуждённо можно написать еще больше еще более сложного кода тестов?
Привет, JustDont! Спасибо за комментарий. Цели моей статьи — рассказать какие преимущества дает TDD, сравнить с test last подходом и показать на упрощенном примере с продакшена, как можно начать разрабатывать по TDD.

В конце статьи я специально оставил опросник, чтобы в случае заинтересованности разобрать более сложные темы и примеры разработки по TDD.
рассказать какие преимущества дает TDD, сравнить с test last подходом и показать на упрощенном примере с продакшена, как можно начать разрабатывать по TDD

Проблема в том, что ваш рассказ о преимуществах никак не коррелирует с приведенным примером. По преимуществам всё шоколадно, а в примере вы приводите тесты, которые во-первых не являются «юнит-» в строгом смысле определения, а во-вторых в половине случаев тестируют трюизмы.
Услышал ваш фидбэк, спасибо! Уже думаю над статьей продвинутого уровня со сложными задачами и тем, как TDD с ними справляется.
а вы как то формализовали что именно вы проверяете в тестах, а что нет? например css расположение элементов? сколько сценариев к компоненту?
есть какие то критерии по которым вы говорите что тут полный тест а тут не полный?
Привет, artemu78. Спасибо за вопросы! Касательно того, что тестируем, как я и сказал в статье, верстку мы тестами не покрываем. Покрывается все, что касается компонентов, логики и работы со стором. Количество сценариев зависит от сложности каждого конкретного компонента. При правильной разработке по TDD у вас не возникают вопросы о полноте тестов или количестве сценариев. Так как сначала вы пишите тест, а затем просто хотите этот тест “позеленить”, в таком случае, ваши тесты всегда будут полными.

Как мне кажется, минус в виде замедления скорости разработки, весьма существенный, с точки зрения бизнеса.

Modin, спасибо за комментарий! Вы правы, как я и сказал в статье, вначале будет заметное снижение скорости, и поэтому важно это бизнесу объяснить. Но в продуктовых компаниях, нацеленных на длительные рост и развитие, важнее не кратковременная просадка, а скорость и качество в длительном будущем. Поэтому в нашей компании, мы рассматриваем и пробуем любые варианты, которые в долгосрочной перспективе принесут нам больше пользы.
Спасибо за статью! Вы молодцы! Я считаю, что использование в команде TDD — это уровень!

Мне тоже нравится TDD, но я скептически отношусь к тестированию UI-компонентов. На мой взгляд, это слишком сложно. А инструменты для тестирования UI настолько наворочены, что из-за этого легко потерять суть. Поэтому я упоролся и попробовал перенести код компонента из примера на обычные объекты. Типа, упрощённая аналогия, чтобы проанализировать код и подумать головой при написании тестов. Сложно сказать, что из этого получилось. Но ясно одно — через TDD я бы так не написал.

TDD — это не про то, что «пиши тесты на всё, пиши тесты впереди, будь героем!». TDD — это про то, что «пиши так, чтобы было максимально легко тестировать». В данном случае, видимо, мощность инструментов сводит TDD на нет.

Упрощённая версия компонента
export interface IModal {
	close(): void;
}

export interface IModalParams {
	opened: boolean;
	width: number;
	title: string;
	buttonText: string;
	onButtonClick: () => void;
}

export class ApplicationPublishModal {
	constructor(
		private appName: string,
		private isPublic: boolean,
		private createModal: (params: IModalParams) => IModal,
		private onPublish: () => void,
	) {}

	// аналог render()
	init(): void {
		const modal = this.createModal({
			opened: true,
			width: 480,
			title: this.getTitle(),
			buttonText: this.getButtonText(),
			onButtonClick: () => {
				this.onPublish();
				modal.close();
			},
		});
	}

	private getTitle(): string {
		return !this.isPublic ? `Publish ${this.appName} app` : `Republish ${this.appName} app`;
	}

	private getButtonText(): string {
		return !this.isPublic ? 'Publish' : 'Republish';
	}
}



Тесты
import { ApplicationPublishModal, IModal, IModalParams } from './q';

export class ModalStub implements IModal, IModalParams {
	opened: boolean = false;
	width: number = 0;
	title: string = '';
	buttonText: string = '';
	onButtonClick: () => void = () => {};
	close(): void {
		this.opened = false;
	}
}

describe('ApplicationPublishModal', () => {
	it('should open modal on init', () => {
		const modal = new ModalStub();
		const applicationPublishModal = createApplicationPublishModal({ modal });
		applicationPublishModal.init();
		expect(modal.opened).toBe(true);
	});

	it('should set modal width', () => {
		const modal = new ModalStub();
		const width = 480;
		const applicationPublishModal = createApplicationPublishModal({ modal });
		applicationPublishModal.init();
		expect(modal.width).toBe(width);
	});

	it('should close modal on modal button click', () => {
		const modal = new ModalStub();
		const applicationPublishModal = createApplicationPublishModal({ modal });
		applicationPublishModal.init();
		modal.onButtonClick();
		expect(modal.opened).toBe(false);
	});

	it('should call onPublish callback on modal button click', () => {
		const modal = new ModalStub();
		const spy = jasmine.createSpy();
		const applicationPublishModal = createApplicationPublishModal({ modal, onPublish: spy });
		applicationPublishModal.init();
		modal.onButtonClick();
		expect(spy).toHaveBeenCalled();
	});

	describe('should provide title to modal', () => {
		it('if app not public', () => {
			const modal = new ModalStub();
			const applicationPublishModal = createApplicationPublishModal({
				modal,
				appName: 'AppName',
				isPublic: false,
			});
			applicationPublishModal.init();
			expect(modal.title).toBe('Publish AppName app');
		});

		it('if app public', () => {
			const modal = new ModalStub();
			const applicationPublishModal = createApplicationPublishModal({
				modal,
				appName: 'AppName',
				isPublic: true,
			});
			applicationPublishModal.init();
			expect(modal.title).toBe('Republish AppName app');
		});
	});

	describe('should provide button text to modal', () => {
		it('if app not public', () => {
			const modal = new ModalStub();
			const applicationPublishModal = createApplicationPublishModal({ modal, isPublic: false });
			applicationPublishModal.init();
			expect(modal.buttonText).toBe('Publish');
		});

		it('if app public', () => {
			const modal = new ModalStub();
			const applicationPublishModal = createApplicationPublishModal({ modal, isPublic: true });
			applicationPublishModal.init();
			expect(modal.buttonText).toBe('Republish');
		});
	});
});

interface ApplicationPublishModalParams {
	appName?: string;
	isPublic?: boolean;
	modal: ModalStub;
	onPublish?: () => void;
}

function createApplicationPublishModal({
	appName = '',
	isPublic = false,
	modal,
	onPublish = () => {},
}: ApplicationPublishModalParams): ApplicationPublishModal {
	return new ApplicationPublishModal(
		appName,
		isPublic,
		(params) => Object.assign(modal, params),
		onPublish,
	);
}




Ну а теперь разбираемся… По сути ApplicationPublishModal — это враппер над переданной/захардкоженой модалкой. Он (почему-то) управляет открытием закрытием модалки, которая по идее могла бы управлять этим сама. Даже текст тестов намекает, что тут что-то не так. Ну и второе, что он делает, это определяет заголовок и текст кнопки. И тут самое оно! Если вы хотите быть уверенными, что заголовок/текст генерируется правильно в зависимости от флажка, вынесите это в отдельную чистую функция и покройте 10 тестами. Это будет супер легко, и по-TDD. Остальное — просто дублирование текста из файла с кодом, в файл с тестами. Не тратье свою жизнь на тестирование прокидывания параметров. Это ж рекурсия!)

Извините, если слишком резко. Но это мой мнение. Вот пример моих тестов по TDD.

Резюме: Вы на правильном пути. Но в системе есть места, типа функции main, которые сложно и бесполезно тестировать. Я считаю UI-компоненты такими местами. Лучший способ продолжать двигаться путем TDD — вытаскивать из компонентов всё что можно, оперировать чистыми функциями и объектами, тренироваться на них. Желаю удачи!
Самое любопытное, что в статье выдвинут тезис, что, мол, TDD лучше, потому что придётся сначала подумать над тем, что писать. И затем в качестве примера выдвигаются тесты, где тестируется прокидывание пропсов в захардкоженный библиотечный компонент.

Страшновато себе представить, что вот это вот и выдаётся за результат «подумали». Что же там было бы без «подумали»?

А юнит-тестирование чистых функций — это очень полезная штука, да. TDD или не TDD — дискуссионно, но сами юнит-тесты в этих случаях очень нужны. Насчёт остального (рендера и т.д.) — я придерживаюсь мысли, что упирать тут в «юнит-» не только бесполезно, но и вредно. Хорошие функциональные тесты реактовских компонентов я видел. Хорошие юнит-тесты — неа. Они или не «юнит», или ничего полезного не тестируют, или и то и другое.

Ещё не учтен момент про 40-90% багов, которые и в самих тестах проскочат, но не будут считаться багами, ведь покрыто)

Согласен, проще было тогда уж написать e2e тест на Cypress, по крайней мере было бы больше уверенности что с изменением компонента ничего не сломалось, критерием что компонент работает будет что-то в реальном браузере.
Привет, Justerest! Спасибо за твой крутой содержательный комментарий и проделанный эксперимент! Понимаю откуда появляются вопросы к примеру, в этой статье я сознательно старался его упростить т.к. хотел, чтобы эта статья стала некоторым мотиватором начать, попробовать экстремальное программирование по TDD. Соответственно сам пример будет интересен в основном новичкам TDD, опытным разработчикам будет интереснее почитать о преимуществах и сравнении test first с test last. Однако теперь я вижу, что разбор более сложных примеров и тем будет интересен многим, поэтому уже начинаю думать над следующей статьей более продвинутого уровня, с упором на примеры.
Кроме того, спасибо за пожелания, и вам желаю удачи!
А есть какая-нибудь ретроспективная статистика? Объективная или хотя бы субъективная?
Тип по ощущением стало меньше мелких багов после изменений, или чё-то такое?
Привет, discopalevo. Спасибо за вопросы! Я думал над добавлением в заключение более точных цифр и графиков, но все-таки не стал перегружать этим статью. Если говорить о багах, то их число, в компонентах написанных по TDD, моментально снизилось и действительно приблизилось к нулю, благодаря чему нам удается не отвлекаться на это в спринте. Если говорить о скорости разработки, то этот график будет более плавным, из спринта в спринт кодовая база на TDD и опыт позволяют нам ускоряться, и если сравнивать емкость спринта сейчас и полгода назад, то при тех же ресурсах, мы делаем примерно в полтора раза больше задач.
Сам подход не нравится. Что будет если например потребуется отобразить предпросмотр перед публикацией и кнопку отменить? Или открыть модальное окно ещё раз?
И компонент Modal может превратиться в монстра с 20 возможными пропсами.

const ApplicationPublishModal = ({ actionLabel, appName, isPublic, publish }) => {
  return (<Modal>
    <Container width={480}>
        <Title>{actionLabel} {appName} app</Title>

        <Button onClick={publish}>
         `${isPublic ? 'Rep' : 'P'}ublish`
        </Button>
      </Container>
    </Modal>
  )
}
Эта методология разработки мотивирует в первую очередь подумать, а не приступить. Это позволяет глубже погрузиться в задачу и не упустить всевозможные корнер-кейсы.

А без TDD и тестов разве не нужно сначала подумать и вникнуть в задачу, перед тем как писать код? Разве не нужно продумывать всевозможные эдж-кейсы?

При test last подходе не нужно много думать — можно сразу переходить к написанию кода.

А если подумать много заранее? получается можно обойтись без TDD?
Т.е. получается какая разница где думать много — перед написанием кода, или перед написанием тестов? Всё-равно подумать много надо в любом случае. Получается TDD — лишнее звено?..)
Если код пишет толпа джуниоров то TDD и Typescript нужны чтобы гарантировать хоть какое-то качество кода.
Нет, это абсолютно никак не поможет.
Просто не надо делать так, чтоб код писала толпа джуниоров без присмотра. Иначе никакие best practices никого не спасут от грядущего «всё переписать», когда станет ясно, что проект неподдерживаем.

Отличная мотивирующая статья!


Автор точно подметил психологические проблемы разработчиков при написании тестов, доступно описал все плюсы подхода TDD и показал на простом примере как это работает и какие результаты дает!


Многим авторам нужно поучиться так писать статьи.
Спасибо автору.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.