Pull to refresh
11
0
Сергей Клевакин @Justerest

User

Send message

В Angular Default тоже было не надо. Но не обманывайте ребятишек, под капотом же как-то View узнает про изменение? Видимо, декоратор и вызывает ручку для перерисовки, когда декорированная функция вызывается?)

А можно глупый ответ? Чтобы не использовать $mol)

Собственно, при написании интерфейса нужно понимать, что паттерн MVC, предполагает, что View как-то должно обновиться при изменении Model.

При стратегии OnPush в Angular View узнает об изменениях Model через паттерн Observable. Никакого волшебства в ChangeDetection. Если хочешь чтобы что-то обновилось, дерни ручку – subject.next(), markForCheck(), setState() (ой, это уже React).

Изначально Angular снимали с разработчика обязанность по логике оповещения об изменениях через zone.js, но решили больше так не делать.

Насколько я понимаю, $mol решает этот вопрос под капотом, не предоставляя разработчику ручного управления процессом ChangeDetection? Это ваше архитектурное решение)

Если вы рисуете uml для людей, а не для галочки, у вас таких примеров не будет. Ограничьте количество элементов, можно настроить длину стрелок и прочее. Диаграмма должна быть понятной - это зависит не от инструмента рисования, а от автора

Статья интересная. Сам постоянно задумываюсь над тем, что зависимость от интерфейса не даёт настоящей инверсии зависимостей.

Но примеров не хватает, как изолировать модель? Как в модели Address правильно избавиться от location API? Сразу принимать zip code в конструктор, насколько я понимаю?

А кто сказал, что там нет декомпозиции? Если интересно, то конечно, у каждого шага свой сервис, который обрабатывает логику своей части. А верхнеуровневый класс нужен, чтобы агрегировать потом совокупность этих шагов + идентификация активного шага. Я просто пытался привести пример, как из страшного большого можно выковыривать что-то поменьше и тестить.

Ни в коем случае! TDD строго запрещает другие виды тестирования!)))

Я не начинаю писать тесты через TDD, если не нахожу удобной абстракции, через которую тесты будут простыми и красиво выражать требования) Нужно подобрать удобный масштаб, который и реализацию не будет сковывать, и намерения поможет выразить.

У меня была задача (фронтенд) – сделать инпут БИК банка с асинхронной валидацией и автозаполнением данных названия банка/кор. счёта через ответ от сервера. Я дня 3 откладывал задачу, потому что не мог найти нужную точку, с которой было бы удобно писать тест. Изначально думал, что начать надо с валидатора – практически чистая функция) Но валидация в Angular настолько специфична, что такие тесты ничего не опишут. В итоге нашёл удобный масштаб – стал тестировать класс формы BankFormGroup целиком. Удобно описывать кейсы, которые эмулируют действия пользователя.

// test: should fill bank data
const fg = new  BankFormGroup(checkBicApiMock);
fg.controls.bic.update('123456789');
expect(fg).toBeInvalid();
expect(fg).toBePending();
wait(1000) // ждём, пока проверка БИК выполнится
expect(fg).toBeValid();
expect(fg.bankName).toBe("Название банка от сервера");


Не знаю, юнит это или интеграционный тест. Но в такой форме мне удобно было описывать кейсы и писать реализацию.

Да, я схитрил, и одним предложением заменил целую эпопею) На меня сильно повлияла вот эта статья про отказ от зависимостей https://habr.com/ru/company/jugru/blog/545482/

Написание тестов требует того, чтобы ваши классы были маленькими и имели минимум зависимостей – это отдельная сложная тема, и удобство написания тестов в какой-то степени служит индикатором того, что ваши классы достаточно маленькие и SOLIDные.

Я понимаю, что во многих частях системы это неудобно или невозможно. В таких случаях я вытаскиваю из большого класса максимум чистых функций, маленьких классов и тестирую их отдельно. Про это и статья выше. Главное, чтобы профит был, а не ради идеи.

Пример из практики – многостраничная форма. (Да, я тупой/модный фронтендер)

Требования: пользователь по шагам заполняет данные, может возвращаться что-то редактировать, между шагами иногда должны пробрасываться данные, на последнем шаге идёт большой POST на сервер.

Понятно, что удобно для этой задачи использовать какой-то единый сервис, который сможет агрегировать итоговые данные из разных шагов, управлять активностью шагов и будет иметь 100500 зависимостей. Даже не надейтесь разработать его по TDD или замокать все зависимости. Там реально они будут сами по себе добавляться.

Поэтому сначала пишем через TDD маленький и простой класс, отвечающий за навигацию по шагам: нужно знать какой шаг активный (`+getActiveStep()`), на какой шаг можно/нельзя перейти (`+isStepAvailable(stepId)`). Используем минимальный интерфейс, который нужен только для этой функциональности – получаем игрушечный класс Stepper и крохотный интерфейс Step. Начинаем использовать нашу игрушку в большом грязном классе MainStepper. Да, большой грязный класс остаётся без тестов, но ответственность за функциональность навигации остаётся в нашем игрушечном классе и хорошо протестирована.

И такими итерациями можно прийти к тому, что наш большой грязный класс превратится в обычный фасад, использующий много маленьких простых протестированных классов/функций. Меня такой расклад в большинстве случаев устраивает.

TDD позволяет написать то, что без TDD написать практически невозможно)

Профит TDD, как я его вижу, в том, что ты описываешь очень маленькими шагами желаемое проведение, реализуешь его в коде и имеешь уверенность, что твой последний шаг не сломал предыдущие. TDD становится полезным, когда этих шагов/требований много, и вы не в состоянии удержать в голове весь набор кейсов.

В процессе написания кода в голову постоянно приходят идеи альтернативных решений. TDD позволяет пробовать их на ходу - если код стал проще и тесты остались зелёными, значит все ок. Грубо говоря, вы начинаете решать задачу через добавление 10 "ифчиков", а потом заменяете это на нормальный алгоритм и убеждаетесь, что поведение осталось равнозначным. И такие повороты можно делать на каждом шаге. И что очень важно, ОЗУ головного мозга прослужит вам дольше, чем если бы вы пытались держать все кейсы в голове разом.

На счёт 100% покрытия - это бред какой-то или фантастика.

По поводу зависимостей, которые мешают писать через TDD... Если пишите что-то через TDD (или просто надо тестировать и прокидывать моки), вытащите этот класс в укромное место, чтобы неожиданные зависимости туда не попадали, сосредоточьтесь на определенной функциональности.

OCP не только про наследование. Он про расширяемость в целом. Вот неплохой пример https://towardsdatascience.com/5-principles-to-write-solid-code-examples-in-python-9062272e6bdc


И у Мартина похожий пример есть, хотя он его больше к SRP относит. Думаю, это потому что принципы связаны.

Я бы не стал наследоваться, оставил в интерфейсе только необходимое. Использую подобные фишки в своих проектах.

И не обязательно использовать EventEmitter для Output событий. Это всего лишь враппер над Observable, который позволяет через флаг указать кидать события синхронно или асинхронно.

class Switch {
	static create({ enabled }: { enabled: boolean } = { enabled: false }): Switch {
		return new Switch(new BehaviorSubject(enabled));
	}

	get enabled(): boolean {
		return this.enabledSubject.value;
	}

	enabled$: Observable<boolean> = this.enabledSubject.asObservable();

	private constructor(private enabledSubject: BehaviorSubject<boolean>) {}

	toggle(): void {
		this.enabledSubject.next(!this.enabled);
	}
}

export class SampleComponent {
	somethingSwitch = Switch.create();

	@Output() somethingSelected = this.somethingSwitch.enabled$()
 ...
}

А если у клиента угнали refresh token? Как отловить злоумышленника, который получает новую пару?

Почему никого не смущает получение злоумышленником доступа на 10-30 минут…

*Пользователь* — Меня взломали! Сделайте что-нибудь!
*Разработчики* — Да это всего на 30 минут. Не переживайте!
И самое страшное, что я до сих пор получаю уведомления к своим открытым ПР-ам. И прямо на глазах мой FizzBuzzEnterprise превращается в функцию из примера исходного кода в статье… Блин, чуваки шарят в садизме…
Ой, история на целую статью… Но если кратко, я устроился в довольно известную компанию, но мой стиль кунг-фу не зашёл остальным членам команды. Я люблю ООП, оборачивать примитивы классами, расписывать задачу через такое количество классов, которое требует SOLID. Команда восприняла такой подход, как не соответствующий общему стилю и переусложненный.

И тут не ясно… То ли вкусы не совпали, ты ли я реально всё переусложняю, то ли они быдлокодеры-олимпиадники)
Представьте, что каждая функция в моем примере, это отдельный файл (отдельный класс). Основная метрика не в количестве строк кода. А в количестве классов, которые придется менять. В идеальном варианте при добавлении нового измерения ничего менять не надо, нужно только добавлять новые классы-файлы.
Я бы сделал примерно так. Написал на функциях для краткости, но тестировать парсер можно и в таком виде без прокидывания зависимостей через конструктор. Убрал микрооптимизации типа генерации общего URL, query параметров. Эти вещи практически не нарушают DRY, даже в исходном варианте до рефакторинга это было преждевременно.

interface Measurement {
	date: Date;
	value: number;
}

interface Response {
	airTemperatures: Measurement[];
	waterTemperatures: Measurement[];
}

export class SimpleDate {
	constructor(private date: Date) {}

	/** Returns date in `DD.MM.YYYY` format  */
	toString(): string {
		return `${this.date.getDate()}.${this.date.getMonth() + 1}.${this.date.getFullYear()}`;
	}
}

async function getMeasurements(from: SimpleDate, to: SimpleDate): Promise<Response> {
	return {
		airTemperatures: await getAirTemperatureMeasurements(from, to),
		waterTemperatures: await getWaterTemperatureMeasurements(from, to),
	};
}

async function getAirTemperatureMeasurements(
	from: SimpleDate,
	to: SimpleDate,
): Promise<Measurement[]> {
	return parseMeasurements(await getHtmlContent(`/air-temperature?from=${from}&to=${to}`));
}

// Покрываем тестами вида "html" -> Measurements[]
// Для специфичных форматов будем создавать подобные парсеры
function parseMeasurements(html: string): Measurement[] {
	return []; // not implemented
}

async function getHtmlContent(url: string): Promise<string> {
	return 'html...'; // not implemented
}

async function getWaterTemperatureMeasurements(
	from: SimpleDate,
	to: SimpleDate,
): Promise<Measurement[]> {
	return parseMeasurements(await getHtmlContent(`/water-temperature?from=${from}&to=${to}`));
}



На мой взгляд, основная ошибка изначального рефакторинга — желание разбить код на слои. Слои получатся сами собой, не надо к ним стремиться. В первую очередь нужно думать про разделение кода по фичам, которые могут меняться, добавляться независимо друг от друга — SRP, OCP.

Возможно, я так же преждевременно вытащил класс SimpleDate, но не люблю подобные вещи (преобразованин даты в строку) писать в процедурном стиле.
Классная тема! Меня на днях из-за такого уволили)
Но по-моему, пример после рефакторинга не стал понятнее, или гибче. Посчитайте, сколько классов придется затронуть при добавлении нового параметра (уровня солнечной активности, например). Что будет, если часть информации придется брать с другого сайта с другим форматом?
Хороший эксперимент, душок пропал, но читаемость снизилась, расширяемость не добавилась.
Так-как я пока безработный, самому хочется эту же задачку расписать) но не обещаю)
Спасибо за статью! Вы молодцы! Я считаю, что использование в команде 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 — вытаскивать из компонентов всё что можно, оперировать чистыми функциями и объектами, тренироваться на них. Желаю удачи!
Я проголосовал за подход SomeOrDefault, он мне нравится, но проверки на null утомляют.

Есть ещё вариант: добавить в сервис вспомогательные методы, которые позволяют проверить потенциальные причины для выброса исключения.

interface UserRepository {
	userExist(userId: string): boolean;
	getUserOrThrow(userId: string): User;
}

// Пример использования:
if (userRepository.userExist(userId)) {
    const user = userRepository.getUserOrThrow(userId);
    // работаем с юзером, будучи уверенными, что всё ок
}
// делаем что-то другое, раз юзера не существует...


Смысл такой, мы не обрабатываем исключения через try/catch, которые бросает метод getUserOrThrow. Если исключение брошено, значит что-то пошло не по плану (500).

Но если нужно (ответить 404, например), мы проверяем причины, по которым может быть вызвано исключение, и обрабатываем их с помощью обычных if/else.

Как вам такой подход?
Круто, спасибо за статью! Я пережил подобный опыт. Это удивительно, но мне любую задачу становится в разы проще решать, если записывать подзадачи на листочек и вычеркивать по одной. Эффект — будто освободил 10ГБ оперативки на компе.

Information

Rating
Does not participate
Location
Екатеринбург, Свердловская обл., Россия
Date of birth
Registered
Activity