company_banner

Тестирование на примере ReactJS: насколько глубока кроличья нора

    Всем привет, меня зовут Ярослав Астафьев, и сегодня я хотел бы провести обзорную экскурсию в тестирование ReactJS. Я не буду углубляться в сложности тестирования веб приложений с использованием определенных библиотек (руководствуясь подходом «сложно тестировать только плохой код»), взамен постараюсь разнообразить ваш кругозор. Так что в этой статье React — скорее повод собрать воедино подходы к тестированию, отправная точка, объединяющая хипстеров и технологии. Корректнее будет даже сказать, что речь пойдет о принципах тестирования вообще с иллюстрациями на ReactJS (и не только).

    Если вы считаете себя гуру тестирования — пропустите первую половину статьи, она о базовых принципах тестирования. Если же и вторая часть не откроет для вас ничего нового — приходите к нам работать и научите как надо.



    Если введение не вызвало приступ синестезии — добро пожаловать под кат.

    Unit-тесты


    Unit Jest — библиотека для тестирования JavaScript. Не нравится эта — возьмите другую, но тогда уже лучше Ava. Здесь все просто: нажимаем на волшебную кнопку и убеждаемся, что некое значение с «0» поменялось на «1»:

    import React from "react"
    import { MyButton } from "../src/components/dummy/myButton"
    import renderer from "react-test-renderer"
    
    test("MyButton has onPress fn", () => {
      let x = 0
      const instance = renderer
        .create(<MyButton onPress={() => x++} />)
        .getInstance()
      expect(instance.handlePress).toBeDefined()
      expect(x).toBe(0)
      instance.props.handlePress()
      expect(x).toBe(1)
    })
    

    Теперь у вас есть все необходимые навыки, чтобы протестировать волшебную кнопку. К сожалению, эти навыки не имеют отношения к реальной жизни. Компонента React не может быть настолько хорошо изолирована, а изоляция — один из главных принципов модульного тестирования. Каким-то образом необходимо убрать все компоненты, которые так или иначе участвуют в методе render, за исключением тестируемой. И есть выход: умные люди для этого придумали mockAPI.

      // initJest.jsx file
      global.fetch = require('jest-fetch-mock')
      //custom mock
      const API = require('mockAPI')
      //static mock
      describe("Date() Tests", () => {
        beforeEach(() => {
          MockDate.set("2011-09-11T00:00:00.000Z")
        })
        afterEach(() => {
          MockDate.reset()
        })
        //smth ...
      })
    

    Суть Mock простая: все что не наше — это Mock/Stub/Fake/Dummy/Spy etc. «Эмулируем» нужным нам образом реальное поведение компоненты, у которой может быть сложная логика, на заранее подготовленных тестовых данных и принимаем на веру, что все эмулированные компоненты работают идеально, если в них на вход подать правильные параметры.

    Для jest есть библиотека jest-fetch-mock, в ней можно определить моки глобально. Если такой вариант не нравится, можно «мОчить» каждую нужную вам в тесте компоненту отдельно.

    Чистая функция на одни и те же входные данные всегда возвращает один и тот же ответ. Соответственно, если у нас в бизнес-логике компоненты есть «не чистые» функции/компоненты, то в юнит-тестах их нужно будет тоже «замОчить» (но для юнит тестов это правило не всегда верно). Классический пример — react-компонента, которая отображает текущую дату и время в нужном вам формате, при каждом запуске тестов дата будет разная, и вы не сможете написать корректные юнит-тесты. Для всех несогласных можно усложнить пример, где ваша компонента должна выводить дату в относительном формате и красненьким подсвечивать даты, которые старше года от текущей даты.

    Соответственно, если у вас есть динамические штуки, которые зависят от времени/погоды/давления, то mock переопределит нужный вам вызов так, чтобы не было зависимости от сторонних факторов. Таким образом, не нужно ждать 29-го февраля, чтобы словить упавший тест.

    Правила unit-тестов


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

    • Детерминизм
    • Изоляция
    • Независимость от внешних факторов
    • Здравый смысл

    Правило первое: все тесты должны быть детерминистическими. Если я написал тест на Windows, то на Mac он тоже должен запуститься и выдать тот же результат. Разработчики на Windows любят забывать, что название файлов в *nix системах чувствительно к регистру. И вам повезет, если тесты упадут в рамках CI, а не приложение в production.

    Следующее правило — изоляция. Мы мочим все не тестируемые компоненты. Если это сделать трудно, значит, пора рефакторить.

    Last but not least: если есть данные, которые ваше приложение получает в рантайме, их тоже нужно прибить гвоздями. Это может быть локаль, размер окна, формат даты, формат чисел с плавающей точкой и т.п.

    Integration тесты


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

    Можно сделать формальный подход: добиться покрытия юнит тестами в 80% (плохо написанные тесты не ревьюить/требовать покрывать тестами только новый или измененный код), потом провести полный аудит и рефакторинг всех написанных тестов с разбором типовых ошибок, формализовать внутренние правила написания тестов и раз в год проводить подобные рейды. Если после всех описанных выше действий ваш unit tests code coverage все еще 80%+ — значит у вас зрелая команда, либо вы просто недостаточно критически относитесь к своему коду/тестам. Если code coverage стал меньше, то нужно добиться покрытия в 80% еще раз и перейти к написанию интеграционных тестов. Можно подойти менее формально и просто руководствоваться здравым смыслом: например, для каждого бага, который воспроизвелся n раз, писать тест или придумать что-нибудь еще, например, подкинуть монетку.

    Второй открытый вопрос: а какие тесты считаются интеграционными? Пожалуй, оставим его без ответа.



    В интеграционных тестах мы тестируем работу не одной компоненты, а нескольких компонент в связке. Никаких правил нет, но здравый смысл подсказывает:

    • не тестировать, что как рендерится, куда вызывается и когда все это кончится;
    • не тестировать работу ReactJS, если он не работает – вам ничего не поможет;
    • не тестировать, как работает стейт-машина React;
    • тестировать бизнес-логику/модель данных/пограничные ситуации/то, что часто ломается.

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

    Другая причина, почему не нужно пытаться протестировать все, — это False Security (самые важные пункты я уже попробовал написать выше). Каждая команда должна прочитать про ошибки первого и второго рода, лемму Неймана-Пирсона и оценить свои риски в деньгах, попугаях или другой мере правды, принятой в команде.

    Но из этого правила, как и из любого другого, есть исключения. Забудьте все, что сказано выше:

    • При тестировании динамических зависимостей. Когда вы не знаете, какая компонента придет в runtime, вы ее рендерите, а она может и не прийти. На это тоже нужно тест написать, circuit bracker никто не отменял. Или придет не та компонента, которую вы ожидали, или сломанная компонента. Поэтому в таком случае пишем интеграционный тест и рендерим. Проверяем, все ли работает, ничего ли не падает.
    • При pixel perfect (ну вы поняли) разработке придется рендерить и делать diff скриншотов, и при каждом обновлении библиотеки компонент на новую версию — обновлять эталонные скриншоты. Потому что легче нанять нового дизайнера смириться, чем исправить.

    Snapshot-тесты


    Самый простой интеграционный тест – это snapshot:

    1. Берем компоненту, рендерим ее
    2. В рендере пишем console.log(this)
    3. Копируем из консоли эталонные данные
    4. Сравниваем

    Если хотите немного заморочиться, то советую поиграться с библиотекой StoryBook. Это библиотека для snapshot-тестов, которая попутно обернула идею StyleGuidist — создание своей дизайн-системы на базе React-компонент.

    Первое правило snapshot-теста: никогда не рассказывайте пытайтесь тестировать данные. Они всегда должны быть статичные, «замОченные» и независимые. Второе правило: сломанный snapshot-тест не означает, что все плохо. Если он красненький — не факт, что все поехало. Есть масса вариантов сделать так, чтобы верстка была одинаковая, но при этом DOM-дерево было разным. Так что игнорируем пробельчики, атрибуты, ключики или не тестируем то, что требует так много времени на поддержку. Либо маркируем руками что сломалось, что нет. Сломанные тесты чиним и перезапускаем StoryBook в режиме обновления mock-ов — режиме, в котором тест будет рендерить компоненты и вставлять Snapshot, как эталонное значение в expect condition.

    xState и React Automata


    ReactJS — сложная штука. Казалось бы, библиотека классная. Три компоненты сделал — класс: и стейт-машина вроде работает и код красивый. Потом пишешь на ReactJS полгода, смотришь на код — ерунда какая-то. Не поймешь, где костыли, где роуты, где стейт, где кэши… Потом думаешь: ну, сделаю как Фейсбук советует: прикручу «хоки», «хуки», еще что-нибудь и вдруг ловишь себя на мысли, как ты шерстишь hh.ru в попытках найти проект с разработкой на реакте с нуля, чтоб там уж точно наверняка все красиво сделать

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

    Здесь стоит вспомнить xState. Это детерминированная машина стейтов для JavaScript. На xState можно сделать очень крутой UI — ссылку на соответствующий доклад можно найти в документации библиотеки React Automata. В свою очередь, React Automata — библиотека, в которой адаптировали xState в ReactJS. Кроме того, она умеет генерировать тесты на состояния стейт-машины.

    Если у нас первая галочка true — горит зеленая лампочка. Если вторая false — то рисуется серая собачка, а React Automata генерирует тесты на все четыре комбинации этих параметров и валидирует собачек и лампочки. Правда, в какой-то момент вы захотите вырубить половину тестов, но сначала будете очень радоваться… В любом случае это удобный взгляд на ваши тесты сбоку, мне он очень напоминает идеи тестирования детерминированным хаосом.

    Cypress


    Со snapshot мы более-менее разобрались, можно идти в сторону end2end. У нас все продукты внутренние, поэтому on-premises решения — единственный для нас вариант. Надеюсь, что у вас есть возможность использовать облачные решения, тогда вам пригодится такая штука как Cypress.


    Раньше вы выбирали фреймворки тестирования, брали либы для assertion, библиотеки, чтобы сложные XML-ки сравнивать. Потом выбирали драйвер и браузер, чтобы это все запускать. Запускали и писали кучу тестов. Все это жрет инфраструктуру, нужно запихивать все в докер, потом прикручивать какую-нибудь штуку, которая смотрит на тесты в динамике, анализирует их, показывает, что не так…



    Ребята из Cypress сделали все это за вас. Они решили несколько задач: настройку рабочего окружения, написание кода, запуск и запись тестов. Если тест сломан, можно выводить скриншот с подсветкой того, что сломалось. Правда, для мобилок не работает, но там, например, есть Detox. Это, конечно, жестко с точки зрения порога вхождения: вам придется подогнать под него свое приложение, переписать кучу файлов и т.п. Но если захотеть, это возможно.

    Soft-тесты


    Есть альтернативные виды тестов, которые и тестами по-хорошему назвать нельзя. Я их называю soft-тесты. Например, линтеры. Пользуются ими в основном фронтендеры (и на явистов еще, бывает, сходит озарение). Есть множество линтеров: ESLint, JSHint, Prettier, Standard, Clinton. Я советую Prettier: быстро, дешево, сердито, просто настраивать, работает «из коробки».

    Если хочется заморочиться, можете настроить ESLint. Приведу классический пример плагинчика для него: когда заказчик находит в вашем коде комментарии с нецензурными выражениями, он обычно ругается. Хитрые разработчики делают комментарии на русском, чтобы заказчик не догадался. Но заказчик догадался… использовать Google-переводчик и выяснил все, что о нем думают разработчики. Выход из ситуации неприятный, возможно, с потерей денег или клиентов. Для этих случаев вы всегда можете разработать плагин для ESLint, который находит «исконно русские» слова в вашем исходном коде и говорит: «ой, извините, reject вашего коммита».

    Прелесть линтеров в JavaScript заключается в том, что их можно поставить на pre commit hook. Лично мне не нравится, что в Prettier не хранится история (хотя с другой стороны и техдолг не копится). С точки зрения статистического анализа кода такие тесты убоги, потому что нельзя увидеть динамику проекта, посмотреть, сколько было ошибок вчера, позавчера. В принципе эта проблема решена в SonarQube, он есть и в облачном решении. Это статистический анализатор кода, который хранит историю прогонов, умеет работать с двумя десятками языков, в том числе даже с PHP (кому как не им нужна железная рука статического анализа? :) ). В нем можно смотреть динамику своих уязвимостей, багов, техдолга и прочее.

    Complexity-тесты


    Фронтендеры пользуются linters, потому что хотят красивые отступы. Complexity – это тоже soft-тесты, которыми можно пытаться проверять качество вашего кода.


    На картинке показано, каким образом я пытаюсь проследить за мыслью джуниора, который сделал pull request. В таких случаях я предлагаю все сносить и строить прямую дорогу.

    Complexity-тесты идут по очень простому принципу: вычисляют цикломатическую сложность алгоритма. Например, читают функцию, находят в ней 10 переменных. Если 10 — наверное, она сложная. Давайте поставим сложность 1. За каждый цикл будем давать 3 очка, за цикл в цикле — 9, за каждый цикл в цикле цикла — 27. Все складываем и говорим: цикломатическая сложность 120, а человек может понять только 6. Смысл этой оценки в том, чтобы субъективно сказать, когда нужно ваш исходный код рефакторить, разбить на кусочки, выделить новые функции и тому подобное. И да, SonarQube в них тоже умеет.

    Альтернативные тесты


    В моем мире альтернативные тесты тоже относятся к soft-тестам. Solidarity — очень полезная штука для онбординга. И не только для фронтендеров, хотя и написана на JavaScript. Она позволяет тестировать рабочее окружение. Раньше нужно было составлять огромные инструкции, с указанием версий языка программирования, библиотек, списка необходимого софта для того, чтобы просто начать, поддерживать все в актуальном состоянии etc. Теперь можно сказать так: «Вот твой компьютер, вот исходный код. Пока Solidarity не пройдет — не приходи». При этом порог входа низкий. Solidarity умеет делать отпечатки настроенного рабочего окружения и позволяет добавлять очень простые правила на валидацию не только установленного софта. А вас бесит, когда подходят со словами: «Ой, извини, у меня там что-то не работает, можешь помочь?..»

    Второй вариант использования (он же основной) — тестирование production environment со словами: «Unit тесты на CI конечно прошли, но конфигурации CI и PROD существенно разнятся. Так что никаких гарантий…». Цель у библиотеки очень простая: выполнить первое правило continuous integration: «у всех должен быть одинаковый environment». Чистый, изолированный, чтобы не было никаких сайд-эффектов, ну или хотя бы чтобы их стало меньше… кого я пытаюсь обманывать?

    API Call


    Бывает, что разработчики поделены на несколько команд — одни пишут фронтенд, другие бэкенд. Вымышленная ситуация, которой не может быть в реальной команде: вчера все работало, а сегодня после двух релизов — фронта и бэка — все сломалось. Кто виноват? Я, как человек с изначально бэкэнд опытом, всегда говорю: фронтендеры. Все просто, это они где-то накосячили, как и всегда. В один прекрасный момент фронтендеры приходят и говорят: «Мы тут почитали пост по ссылочке, прошли гайд и научились делать слепки вашего REST API. И вы не поверите, оно изменилось…». В общем, если ваши бэкендеры не дружат со Swagger, openAPI или другими подобными решениями — стоит взять на заметку.

    Performance JS


    И наконец, тест performance JS. Никто не тестирует performance JS кроме производителей браузеров. Как правило, они все используют Benchmark.js. «Ой, а мы наш Explorer точили 18 лет, чтобы он табличку миллиард на миллиард отображал быстрее, чем в Chrome». Кому вообще нужна такая табличка?

    Если вы захотите делать тесты производительности — лучше идти другим путем: тестировать end-to-end и смотреть, как все работает. Пользователь формирует восприятие о том, как приложение работает в целом, пользователю не важно, что это проблемы на стороне бекенда.

    War Story #1


    Теперь пример из жизни. Приходят как-то к нам начальники и говорят: «У вас фронт работает очень плохо, еле-еле грузится. Надо что-то делать с перфомансом, народ жалуется». Мы думаем: сейчас придется две недели перфоманс тюнить, ковыряться в логах why did you update, ковырять tree shaking, резать все на chunk'и с динамической подгрузкой… А вдруг не выгорит, вдруг все сломаем или только хуже сделаем? Нужно сделать альтернативное решение. Открываем браузер, смотрим: 7,5 Мб, 2 секунды, все хорошо.



    Поставим Nginx GZip:



    У Nginx есть возможность настраивать степень сжатия, попробуем-ка:



    Прирост — 25% производительности. Останавливаться рано. Взглянем на маленькое лого дизайнера в углу. Оно остается очень красивым, даже если его растянуть, но зачем нам это?



    Вот что получили после оптимизации одной картинки. Вес лого можете оценить сами. Наконец, приходим мы к заказчику и говорим: «первая загрузка не так важна, как повторная. И, включаем принудительное кеширование:



    … Все счастливы, все ликуют!». Кроме пользователя конечно же.

    В итоге решили проводить size аудит почаще. Gzip, шрифты, картинки, стили — те места куда редко кто смотрит, а пользы много.

    Madge и updtrJS


    Следующий шаг: аудит зависимостей. Madge — это такая штука, которая анализирует код и говорит: вот класс такой-то связан с таким-то и т.д. Если все проходит через одну компоненту и она сломается, то будет мало приятного. Madge — отличный инструмент для визуализации, но подходит только для ручного исследования. У него есть такая опция, circular, которая ищет все циклические зависимости в вашем проекте. Если таковые имеются — плохо, если нет — значит еще не написали.

    Боль устаревших фреймворков и библиотек почти решена при помощи updtrJS.
    У вас 70 тысяч строк кода? Вы пытаетесь переехать с 13-го React на 16-ый? Updtr не поможет вам переехать, но зато поможет сказать, на какие версии библиотек вы сможете переехать безболезненно. Кроме того, он позволяет разработчикам оставаться в тренде, помогает держать зависимости up-to-date. Если у вас хорошее покрытие тестами, рекомендую.

    Static Types


    Используйте статическую типизацию JS, потому что динамическая типизация JS – это вообще не фича, это пропасть. Flow, typeScript, reasonML вот это всё…

    Chaos-тестирование


    Суть chaos-тестирования проста: запускаем браузер и начинаем тыкать все, что тыкается, вводить во все поля все, что вводится, и так далее. Пока не сломается. В свое время этот продукт был написан Amazon, чтобы получить как можно больше exceptions на бэкенде. Мол, «давай ты будешь по фронту стучать, а мы будем на бэке смотреть ошибочки». Если ошибки найдутся, то мы пофиксим. Реализации: Gremlin.com и Chaos Monkey.

    В React оно обрело новый смысл — начиная с 16-й версии, в которой добавили componentDidCatch. Если ваш фронт упал, выкинул exception, вы можете это отловить и залогировать. Хотя есть и более элегантные способы решения этой проблемы.

    В рамках проекта Naughty String добровольцы собирают все плохие строки и не только строки, которые могут привести к поломкам или другим неожиданным результатам, позволяет существенно обогатить диапозон ваших тестов. Например, если в твиттере запостить «пробел нулевой длины», он ответит internal server error, что уже тогда говорить о наших коленочных поделках?



    War Story #2



    Это вероятность падения одного моего теста – одна десятимиллиардная. И это событие произошло.

    def generateRandomPostfix(String prefix) {
        return prefix + "-" + Math.abs(new Random().nextInt()).toString()
    }
    
    def "testCorrectRandomPostfix"(){
        given:
            def prefix = "ASD"
        when:
            def result = generateRandomPostfix(prefix)
        then:
            result?.matches(/[a-zA-Z]++-[0-9]++/)
    }
    

    Это был большой и сложный тест. В коде выше я постарался оставить только основную идею. Было не очень просто разобраться, но мы нашли проблему.

    Разберем по шагам. У нас есть префикс ASD, есть функция, генерирующая рандомный постфикс и прибавляющая его к префиксу через дефис. Причем постфикс у нас строго цифровой. Дальше проверяем регулярным выражением корректность сгенерированного результата (забыл сказать, этот тест на языке groovy). В Java самый маленький integer на 1 больше по модулю самого большого integer. Поэтому модулем от самого маленького integer будет самый маленький integer — переполнение буфера еще никто не отменял.





    Вверху код, который убивает браузер. Внизу фикс. Видите хотя бы одно отличие? Это «+» перед fromX. Суть в том, что в какой-то момент формат бэкэнда изменился, XML начал присылать стоки.

    От этого никто не застрахован.

    Параметризированный хаос


    Иногда вас может спасти параметризированный хаос. TestCheck.JS — одна из самых прикольных библиотек с точки зрения хаоса. Она поддерживает, наверное, все фреймворки для тестирования. И бонусом — Flow-To-Gen: если вы описали во Flow типы данных, то он может нагенерировать тесты за вас.

    check(
      property(
        gen.int, gen.int,
        (a, b) => a + b >= a && a + b >= b
      )
    )
    

    { result: false,
      failingSize: 2,
      numTests: 3,
      fail: [ 2, -1 ],
      shrunk:
       { totalNodesVisited: 2,
         depth: 1,
         result: false,
         smallest: [ 0, -1 ] 
       } 
    }
    

    У TestCheck есть много генераторов «из коробки», но можно написать и свои. Функция выше проверяет, что сумма двух чисел всегда больше каждого из них. Естественно, это неправда, простой пример: 0 и -1. Эта штука обнаружила ошибку на значениях 2 и -1, но не остановилась и нашла самый примитивный вариант, на котором можно воспроизвести эту ошибку. Очень круто!

    Другое тестирование


    Ручками тоже нужно тыкать. Например надо тестировать стейт. Стейт бывает не только хорошим, а люди про это часто не думают. Что делать, когда у вас старые данные и новый стейт? когда у вас нет интернет-соединения? нет permission’ов? проблемы с локалью? Можно тестировать разные устройства, платформы, мобилки, залипание клавиш, поддержку скринридеров и многое другое.

    Не стоит забывать о тестировании 3rd party failures. „Так это сторонняя библиотека там сломалась“ — это тоже ваша проблема.

    Production-тесты


    Для production в 16-ом React можно использовать два простых решения: ErrorCeption и HoneyBadger (у нас правда Sentry). Подключаешь библиотеку, и они начинают собирать статистику по ошибкам в продакшн.

    Optimizely делает А/Б-тестирование. Очень круто организовано, умеет делать доставку для конкретных людей, конкретного времени, контента, тестовой выборки, одновременно балансировать нагрузку и вести статистику.

    Out of the box


    JS — не панацея. Ломаться может много чего, и тестировать можно не только на JavaScript. Очень простая штука — validator.w3.org/checklink. Это кроулер, который идет к вам на сайт, смотрит, какие ссылки есть на страницах, и проверяет их работоспособность.

    Если твой сайт доступен из Индии, это не значит, что его можно открыть в России. Achecker.ca/checker должен помочь. Webpagetest.org — проект, без которого мне сложно жить. Tools.pingdom.com — еще один интересный проект. На www.w3.org/WAI/ER/tools можно найти тысячи решений для различных кейсов.

    На прошлой работе мы гоняли тесты на каждый коммит. Коммиты были там на кучу всего. Слава богу, что тогда еще не было Jenkins Multibranch Plugin, который после мержа пулл-реквеста пересобирал все пулл-реквесты. Пользуйтесь тест-сьютами, цепочками тестов («зачем это запускать, если это заведомо не работает»), nightly-билдами, тест-паками regress, full regress, smart regress и т.п.

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

    На этом кратком описании я, пожалуй, закончу. Готов ответить на вопросы в комментариях.
    Сбербанк
    233,00
    Компания
    Поделиться публикацией

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

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

    Самое читаемое