One place for hosting & domains

      использованием

      Запуск нескольких версий PHP на одном сервере с использованием Apache и PHP-FPM в Debian 10


      Автор выбрал COVID-19 Relief Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Веб-сервер Apache использует виртуальные хосты для управления несколькими доменами в одной системе. PHP-FPM использует демона для управления несколькими версиями PHP в одной системе. Вы можете использовать Apache и PHP-FPM для одновременного хостинга на одном сервере нескольких веб-приложений PHP на основе разных версий PHP. Эта возможность полезна, поскольку разным приложениям могут требоваться разные версии PHP, но некоторые серверные комплексы, в том числе стек LAMP в стандартной конфигурации, могут работать только с одной версией. Сочетание Apache с PHP-FPM более экономично по сравнению с хостингом каждого приложения на отдельном экземпляре сервера.

      Также PHP-FPM предлагает разные варианты конфигурации для регистрации данных stderr и stdout, аварийной перезагрузки и адаптивного создания процессов, что полезно для сайтов с высокой нагрузкой. Использование Apache с PHP-FPM — один из лучших вариантов хостинга приложений PHP, особенно с точки зрения производительности.

      В этом обучающем руководстве мы настроим два сайта PHP для работы на одном экземпляре сервера. Каждый сайт будет использовать собственный домен, и на каждом домене будет использоваться собственная версия PHP. Первый сайт site1.your_domain развернет PHP 7.0. Второй сайт site2.your_domain развернет PHP 7.2.

      Предварительные требования

      • Один сервер Debian 10 с не менее чем 1 ГБ оперативной памяти, настроенный согласно руководству Начальная настройка сервера Debian 10, с пользователем non-root user с привилегиями sudo и брандмауэром.
      • Веб-сервер Apache, установленный и настроенный в соответствии с указаниями руководства Установка веб-сервера Apache в Debian 10.
      • Доменное имя, настроенное так, чтобы указывать на ваш сервер Debian 10. Информацию о том, как сделать так, чтобы домены указывали на дроплеты DigitalOcean, можно найти в руководстве Создание указаний на серверы имен DigitalOcean из общих реестров доменов. Для целей настоящего обучающего руководства мы используем два субдомена, каждый из которых указан с записью A в наших настройках DNS: site1.your_domain и site2.your_domain.

      Шаг 1 — Установка PHP версий 7.0 и 7.2 с помощью PHP-FPM

      Выполнив предварительные требования, вы можете установить PHP версий 7.0 и 7.2, а также PHP-FPM и некоторые дополнительные расширения. Для этого предварительно необходимо добавить в систему репозиторий sury php.

      Вначале установите требуемые пакеты, в том числе curl, wget и gnupg2:

      • sudo apt-get install curl wget gnupg2 ca-certificates lsb-release apt-transport-https -y

      Вышеуказанные пакеты позволяют получить безопасный доступ к репозиторию sury php. sury php — это сторонний репозиторий или PPA (архив персональных пакетов). Он предоставляет PHP 7.4, 7.3, 7.2, 7.1 и 7.0 для операционной системы Debian. Также он включает более актуальные версии PHP, чем содержащиеся в официальных репозиториях Debian 10, и позволяет устанавливать несколько версий PHP в одной системе.

      Затем импортируйте ключ пакета:

      • wget https://packages.sury.org/php/apt.gpg
      • sudo apt-key add apt.gpg

      Теперь добавьте в систему репозиторий sury php:

      • echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php7.list

      Обновите репозиторий:

      Установите php7.0, php7.0-fpm, php7.0-mysql, libapache2-mod-php7.0 и libapache2-mod-fcgid с помощью следующих команд:

      • sudo apt-get install php7.0 php7.0-fpm php7.0-mysql libapache2-mod-php7.0 libapache2-mod-fcgid -y
      • php7.0 — это метапакет, который можно использовать для запуска приложений PHP.
      • php7.0-fpm предоставляет интерпретатор Fast Process Manager, который работает как демон и принимает запросы Fast/CGI.
      • php7.0-mysql связывает PHP с базой данных MySQL.
      • libapahce2-mod-php7.0 предоставляет модуль PHP для веб-сервера Apache.
      • libapache2-mod-fcgid содержит mod_fcgid, запускающий несколько экземпляров программы CGI для обработки одновременных запросов.

      Повторите процедуру для PHP версии 7.2. Установите php7.2, php7.2-fpm, php7.2-mysql и libapache2-mod-php7.2.

      • sudo apt-get install php7.2 php7.2-fpm php7.2-mysql libapache2-mod-php7.2 -y

      После установки обеих версий PHP запустите службу php7.0-fpm:

      • sudo systemctl start php7.0-fpm

      Затем проверьте статус службы php7.0-fpm:

      • sudo systemctl status php7.0-fpm

      Вывод должен выглядеть так:

      Output

      ● php7.0-fpm.service - The PHP 7.0 FastCGI Process Manager Loaded: loaded (/lib/systemd/system/php7.0-fpm.service; enabled; vendor preset: enabled) Active: active (running) since Sat 2020-04-04 08:51:47 UTC; 1min 17s ago Docs: man:php-fpm7.0(8) Main PID: 13016 (php-fpm7.0) Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec" Tasks: 3 (limit: 1149) Memory: 19.1M CGroup: /system.slice/php7.0-fpm.service ├─13016 php-fpm: master process (/etc/php/7.0/fpm/php-fpm.conf) ├─13017 php-fpm: pool www └─13018 php-fpm: pool www Apr 04 08:51:47 debian10 systemd[1]: Starting The PHP 7.0 FastCGI Process Manager... Apr 04 08:51:47 debian10 systemd[1]: Started The PHP 7.0 FastCGI Process Manager.

      Повторите процедуру и запустите службу php7.2-fpm:

      • sudo systemctl start php7.2-fpm

      Затем проверьте статус службы php7.2-fpm:

      • sudo systemctl status php7.2-fpm

      Вывод должен выглядеть так:

      Output

      ● php7.2-fpm.service - The PHP 7.2 FastCGI Process Manager Loaded: loaded (/lib/systemd/system/php7.2-fpm.service; enabled; vendor preset: enabled) Active: active (running) since Sat 2020-04-04 08:52:52 UTC; 1min 32s ago Docs: man:php-fpm7.2(8) Process: 22207 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/7.2/fpm/pool.d/www.conf 72 (code=exite Main PID: 22204 (php-fpm7.2) Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec" Tasks: 3 (limit: 1149) Memory: 12.0M CGroup: /system.slice/php7.2-fpm.service ├─22204 php-fpm: master process (/etc/php/7.2/fpm/php-fpm.conf) ├─22205 php-fpm: pool www └─22206 php-fpm: pool www Apr 04 08:52:52 debian10 systemd[1]: Starting The PHP 7.2 FastCGI Process Manager... Apr 04 08:52:52 debian10 systemd[1]: Started The PHP 7.2 FastCGI Process Manager.

      В заключение необходимо активировать несколько модулей, чтобы служба Apache2 могла работать с несколькими версиями PHP:

      • sudo a2enmod actions fcgid alias proxy_fcgi
      • actions используется для выполнения скриптов CGI на основе типа носителя или метода запроса.

      • fcgid — это высокопроизводительная альтернатива mod_cgi, запускающая достаточное количество экземпляров программы CGI для одновременной обработки запросов.

      • alias позволяет создавать схемы разных деталей файловой системы хоста в дереве документов и для целей переадресации URL.

      • proxy_fcgi позволяет Apache перенаправлять запросы PHP-FPM.

      Перезапустите службу Apache, чтобы применить изменения:

      • sudo systemctl restart apache2

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

      Шаг 2 — Создание структур директорий для обоих сайтов

      В этом разделе мы создадим корневую директорию документов и страницу индекса для каждого из двух сайтов.

      Вначале создайте корневые директории документов для site1.your_domain и site2.your_domain:

      • sudo mkdir /var/www/site1.your_domain
      • sudo mkdir /var/www/site2.your_domain

      По умолчанию веб-сервер Apache работает как пользователь www-data и группа www-data. Чтобы убедиться в правильности структуры владения и разрешений для корневых директорий вашего сайта, используйте следующие команды:

      • sudo chown -R www-data:www-data /var/www/site1.your_domain
      • sudo chown -R www-data:www-data /var/www/site2.your_domain
      • sudo chmod -R 755 /var/www/site1.your_domain
      • sudo chmod -R 755 /var/www/site2.your_domain

      Далее вы создадите файл info.php в корневой директории каждого сайта. В нем будет отображаться информация о версии PHP для каждого сайта. Начнем с site1:

      • sudo nano /var/www/site1.your_domain/info.php

      Добавьте следующую строку:

      /var/www/site1.your_domain/info.php

      <?php phpinfo(); ?>
      

      Сохраните и закройте файл. Скопируйте созданный файл info.php в site2:

      • sudo cp /var/www/site1.your_domain/info.php /var/www/site2.your_domain/info.php

      Теперь на вашем веб-сервере должны иметься корневые директории документов, которые требуются каждому сайту для предоставления данных посетителям. Далее мы настроим веб-сервер Apache для работы с двумя разными версиями PHP.

      Шаг 3 — Настройка Apache для обоих сайтов

      В этом разделе мы создадим два файла конфигурации виртуального хоста. Это позволит двум нашим сайтам одновременно работать с двумя разными версиями PHP.

      Для обслуживания этого контента Apache необходимо создать файл виртуального хоста с правильными директивами. Вместо изменения файла конфигурации по умолчанию /etc/apache2/sites-available/000-default.conf мы создадим два новых файла в директории /etc/apache2/sites-available/.

      Вначале создайте новый файл конфигурации виртуального хоста для сайта site1.your_domain. Здесь вы предписываете Apache использовать для рендеринга содержимого php7.0:

      • sudo nano /etc/apache2/sites-available/site1.your_domain.conf

      Добавьте в файл следующее. Убедитесь, что путь к директории сайта, имя сервера и версия PHP соответствуют вашей системе:

      /etc/apache2/sites-available/site1.your_domain.conf

      
      <VirtualHost *:80>
           ServerAdmin admin@site1.your_domain
           ServerName site1.your_domain
           DocumentRoot /var/www/site1.your_domain
           DirectoryIndex info.php
      
           <Directory /var/www/site1.your_domain>
              Options Indexes FollowSymLinks MultiViews
              AllowOverride All
              Order allow,deny
              allow from all
           </Directory>
      
          <FilesMatch .php$>
            # For Apache version 2.4.10 and above, use SetHandler to run PHP as a fastCGI process server
            SetHandler "proxy:unix:/run/php/php7.0-fpm.sock|fcgi://localhost"
          </FilesMatch>
      
           ErrorLog ${APACHE_LOG_DIR}/site1.your_domain_error.log
           CustomLog ${APACHE_LOG_DIR}/site1.your_domain_access.log combined
      </VirtualHost>
      

      В этом файле вы изменили директорию на DocumentRoot, а ServerAdmin на адрес электронной почты, доступный администратору сайта your_domain. Также вы изменили параметр ServerName, устанавливающий базовый домен для этой конфигурации виртуального хоста, и добавили директиву SetHandler для запуска PHP как сервера процессов fastCGI.

      Сохраните и закройте файл.

      Теперь создайте новый файл конфигурации виртуального хоста для сайта site2.your_domain. Для этого субдомена мы будем развертывать php7.2:

      • sudo nano /etc/apache2/sites-available/site2.your_domain.conf

      Добавьте в файл следующее. Убедитесь, что путь к директории сайта, имя сервера и версия PHP соответствуют уникальным параметрам вашей системы:

      /etc/apache2/sites-available/site2.your_domain.conf

      <VirtualHost *:80>
           ServerAdmin admin@site2.your_domain
           ServerName site2.your_domain
           DocumentRoot /var/www/site2.your_domain
           DirectoryIndex info.php  
      
           <Directory /var/www/site2.your_domain>
              Options Indexes FollowSymLinks MultiViews
              AllowOverride All
              Order allow,deny
              allow from all
           </Directory>
      
          <FilesMatch .php$>
            # For Apache version 2.4.10 and above, use SetHandler to run PHP as a fastCGI process server
            SetHandler "proxy:unix:/run/php/php7.2-fpm.sock|fcgi://localhost"
          </FilesMatch>
      
           ErrorLog ${APACHE_LOG_DIR}/site2.your_domain_error.log
           CustomLog ${APACHE_LOG_DIR}/site2.your_domain_access.log combined
      </VirtualHost>
      

      Сохраните файл и закройте его после завершения. Проверьте файл конфигурации Apache на наличие синтаксических ошибок:

      • sudo apachectl configtest

      Вывод должен выглядеть так:

      Output

      Syntax OK

      Активируйте оба файла конфигурации виртуального хоста:

      • sudo a2ensite site1.your_domain
      • sudo a2ensite site2.your_domain

      Отключите сайт по умолчанию, поскольку он не потребуется:

      • sudo a2dissite 000-default.conf

      Перезапустите службу Apache, чтобы применить изменения:

      • sudo systemctl restart apache2

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

      Шаг 4 — Тестирование сайтов

      Мы настроили два сайта для работы с двумя разными версиями PHP. Теперь проверим результаты.

      Откройте в браузере сайты http://site1.your_domain и http://site2.your_domain. Вы увидите две страницы, выглядящие следующим образом:

      Информационная страница PHP 7.0Информационная страница PHP 7.2

      Обратите внимание на заголовки. На первой странице указано, что на сайте site1.your_domain развернута версия PHP 7.0. На второй странице указано, что на сайте site2.your_domain развернута версия PHP 7.2.

      Мы протестировали сайты и теперь можем удалить файлы info.php. Эти файлы представляют собой угрозу безопасности, поскольку они содержат важную информацию о вашем сервере и при этом доступны неуполномоченным пользователям. Чтобы удалить оба файла, запустите следующие команды:

      • sudo rm -rf /var/www/site1.your_domain/info.php
      • sudo rm -rf /var/www/site2.your_domain/info.php

      Теперь у вас имеется один сервер Debian 10, обслуживающий два сайта с разными версиями PHP. Однако PHP-FPM можно применять и для других целей.

      Заключение

      Мы объединили виртуальные хосты и PHP-FPM для обслуживания нескольких сайтов и нескольких версий PHP на одном сервере. Количество сайтов PHP и версий PHP, которые может обслуживать ваш сервер Apache, зависит исключительно от вычислительной мощности вашего экземпляра.

      Теперь вы можете начать изучение более сложных функций PHP-FPM, таких как процесс адаптивного создания или функции регистрации sdtout и stderr. Также вы можете заняться защитой своих сайтов. Для этого используйте наш обучающий модуль по защите сайтов с помощью бесплатных сертификатов TLS/SSL от Let’s Encrypt.



      Source link

      Запуск нескольких версий PHP на одном сервере с использованием Apache и PHP-FPM в Ubuntu 18.04


      Автор выбрал COVID-19 Relief Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Веб-сервер Apache использует виртуальные хосты для управления несколькими доменами в одной системе. PHP-FPM использует демона для управления несколькими версиями PHP в одной системе. Вы можете использовать Apache и PHP-FPM для одновременного хостинга на одном сервере нескольких веб-приложений PHP на основе разных версий PHP. Эта возможность полезна, поскольку разным приложениям могут требоваться разные версии PHP, но некоторые серверные комплексы, в том числе стек LAMP в стандартной конфигурации, могут работать только с одной версией. Сочетание Apache с PHP-FPM более экономично по сравнению с хостингом каждого приложения на отдельном экземпляре сервера.

      Также PHP-FPM предлагает разные варианты конфигурации для регистрации данных stderr и stdout, аварийной перезагрузки и адаптивного создания процессов, что полезно для сайтов с высокой нагрузкой. Использование Apache с PHP-FPM — один из лучших вариантов хостинга приложений PHP, особенно с точки зрения производительности.

      В этом обучающем руководстве мы настроим два сайта PHP для работы на одном экземпляре сервера. Каждый сайт будет использовать собственный домен, и на каждом домене будет использоваться собственная версия PHP. Первый сайт site1.your_domain развернет PHP 7.0. Второй сайт site2.your_domain развернет PHP 7.2.

      Предварительные требования

      • Один сервер Ubuntu 18.04 с не менее чем 1 ГБ оперативной памяти, настроенный согласно руководству Начальная настройка сервера Ubuntu 18.04, с пользователем non-root user с привилегиями sudo и брандмауэром.
      • Веб-сервер Apache, установленный и настроенный в соответствии с указаниями руководства Установка веб-сервера Apache в Ubuntu 18.04.
      • Доменное имя, настроенное так, чтобы указывать на ваш сервер Ubuntu 18.04. Информацию о том, как сделать так, чтобы домены указывали на дроплеты DigitalOcean, можно найти в руководстве Как создать указания на серверы имен DigitalOcean из общих реестров доменов. Для целей настоящего обучающего руководства мы используем два субдомена, каждый из которых указан с записью A в наших настройках DNS: site1.your_domain и site2.your_domain.

      Шаг 1 — Установка PHP версий 7.0 и 7.2 с помощью PHP-FPM

      Выполнив предварительные требования, вы можете установить PHP версий 7.0 и 7.2, а также PHP-FPM и некоторые дополнительные расширения. Для этого предварительно необходимо добавить в систему репозиторий Ondrej PHP.

      Запустите команду apt-get для установки software-properties-common:

      • sudo apt-get install software-properties-common -y

      Пакет software-properties-common предоставляет утилиту командной строки apt-add-repository, которую мы используем для добавления репозитория ondrej/php PPA (архив персональных пакетов).

      Добавьте в систему репозиторий ondrej/php. Репозиторий ondrej/php PPA содержит более актуальные версии PHP, чем официальные репозитории Ubuntu, а также позволяет устанавливать несколько версий PHP в одной системе:

      • sudo add-apt-repository ppa:ondrej/php

      Обновите репозиторий:

      Установите php7.0, php7.0-fpm, php7.0-mysql, libapache2-mod-php7.0 и libapache2-mod-fcgid с помощью следующих команд:

      • sudo apt-get install php7.0 php7.0-fpm php7.0-mysql libapache2-mod-php7.0 libapache2-mod-fcgid -y
      • php7.0 — это метапакет, используемый для запуска приложений PHP.
      • php7.0-fpm предоставляет интерпретатор Fast Process Manager, который работает как демон и принимает запросы Fast/CGI.
      • php7.0-mysql связывает PHP с базой данных MySQL.
      • libapahce2-mod-php7.0 предоставляет модуль PHP для веб-сервера Apache.
      • libapache2-mod-fcgid содержит mod_fcgid, запускающий несколько экземпляров программы CGI для обработки одновременных запросов.

      Повторите процедуру для PHP версии 7.2. Установите php7.2, php7.2-fpm, php7.2-mysql и libapache2-mod-php7.2:

      • sudo apt-get install php7.2 php7.2-fpm php7.2-mysql libapache2-mod-php7.2 -y

      После установки обеих версий PHP запустите службу php7.0-fpm:

      • sudo systemctl start php7.0-fpm

      Затем проверьте статус службы php7.0-fpm:

      • sudo systemctl status php7.0-fpm

      Вывод должен выглядеть так:

      Output

      ● php7.0-fpm.service - The PHP 7.0 FastCGI Process Manager Loaded: loaded (/lib/systemd/system/php7.0-fpm.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2020-03-29 12:53:23 UTC; 15s ago Docs: man:php-fpm7.0(8) Process: 20961 ExecStopPost=/usr/lib/php/php-fpm-socket-helper remove /run/php/php-fpm.sock /etc/php/7.0/fpm/pool.d/www.conf 70 (code=exited, Process: 20979 ExecStartPost=/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/7.0/fpm/pool.d/www.conf 70 (code=exite Main PID: 20963 (php-fpm7.0) Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec" Tasks: 3 (limit: 1150) CGroup: /system.slice/php7.0-fpm.service ├─20963 php-fpm: master process (/etc/php/7.0/fpm/php-fpm.conf) ├─20977 php-fpm: pool www └─20978 php-fpm: pool www

      Повторите процедуру и запустите службу php7.2-fpm:

      • sudo systemctl start php7.2-fpm

      Проверьте статус службы php7.2-fpm:

      • sudo systemctl status php7.2-fpm

      Вывод должен выглядеть так:

      Output

      ● php7.2-fpm.service - The PHP 7.2 FastCGI Process Manager Loaded: loaded (/lib/systemd/system/php7.2-fpm.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2020-03-29 12:53:22 UTC; 45s ago Docs: man:php-fpm7.2(8) Main PID: 20897 (php-fpm7.2) Status: "Processes active: 0, idle: 2, Requests: 0, slow: 0, Traffic: 0req/sec" Tasks: 3 (limit: 1150) CGroup: /system.slice/php7.2-fpm.service ├─20897 php-fpm: master process (/etc/php/7.2/fpm/php-fpm.conf) ├─20909 php-fpm: pool www └─20910 php-fpm: pool www

      В заключение необходимо активировать несколько модулей, чтобы служба Apache2 могла работать с несколькими версиями PHP:

      • sudo a2enmod actions fcgid alias proxy_fcgi
      • actions используется для выполнения скриптов CGI на основе типа носителя или метода запроса.

      • fcgid — это высокопроизводительная альтернатива mod_cgi, запускающая достаточное количество экземпляров программы CGI для одновременной обработки запросов.

      • alias позволяет создавать схемы разных деталей файловой системы хоста в дереве документов и для целей переадресации URL.

      • proxy_fcgi позволяет Apache перенаправлять запросы PHP-FPM.

      Перезапустите службу Apache, чтобы применить изменения:

      • sudo systemctl restart apache2

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

      Шаг 2 — Создание структур директорий для обоих сайтов

      В этом разделе мы создадим корневую директорию документов и страницу индекса для каждого из двух сайтов.

      Вначале создайте корневые директории документов для site1.your_domain и site2.your_domain:

      • sudo mkdir /var/www/site1.your_domain
      • sudo mkdir /var/www/site2.your_domain

      По умолчанию веб-сервер Apache работает как пользователь www-data и группа www-data. Чтобы убедиться в правильности структуры владения и разрешений для корневых директорий вашего сайта, используйте следующие команды:

      • sudo chown -R www-data:www-data /var/www/site1.your_domain
      • sudo chown -R www-data:www-data /var/www/site2.your_domain
      • sudo chmod -R 755 /var/www/site1.your_domain
      • sudo chmod -R 755 /var/www/site2.your_domain

      Далее вы создадите файл info.php в корневой директории каждого сайта. В нем будет отображаться информация о версии PHP для каждого сайта. Начнем с site1:

      • sudo nano /var/www/site1.your_domain/info.php

      Добавьте следующую строку:

      /var/www/site1.your_domain/info.php

      <?php phpinfo(); ?>
      

      Сохраните и закройте файл. Скопируйте созданный файл info.php в site2:

      • sudo cp /var/www/site1.your_domain/info.php /var/www/site2.your_domain/info.php

      Теперь на вашем веб-сервере должны иметься корневые директории документов, которые требуются каждому сайту для предоставления данных посетителям. Далее мы настроим веб-сервер Apache для работы с двумя разными версиями PHP.

      Шаг 3 — Настройка Apache для обоих сайтов

      В этом разделе мы создадим два файла конфигурации виртуального хоста. Это позволит двум нашим сайтам одновременно работать с двумя разными версиями PHP.

      Для обслуживания этого контента Apache необходимо создать файл виртуального хоста с правильными директивами. Вместо изменения файла конфигурации по умолчанию /etc/apache2/sites-available/000-default.conf, мы создадим два новых файла в директории /etc/apache2/sites-available/.

      Вначале создайте новый файл конфигурации виртуального хоста для сайта site1.your_domain. Здесь вы предписываете Apache использовать для рендеринга содержимого php7.0:

      • sudo nano /etc/apache2/sites-available/site1.your_domain.conf

      Добавьте в файл следующее: Убедитесь, что путь к директории сайта, имя сервера и версия PHP соответствуют вашей системе:

      /etc/apache2/sites-available/site1.your_domain.conf

      
      <VirtualHost *:80>
           ServerAdmin admin@site1.your_domain
           ServerName site1.your_domain
           DocumentRoot /var/www/site1.your_domain
           DirectoryIndex info.php
      
           <Directory /var/www/site1.your_domain>
              Options Indexes FollowSymLinks MultiViews
              AllowOverride All
              Order allow,deny
              allow from all
           </Directory>
      
          <FilesMatch .php$>
            # For Apache version 2.4.10 and above, use SetHandler to run PHP as a fastCGI process server
            SetHandler "proxy:unix:/run/php/php7.0-fpm.sock|fcgi://localhost"
          </FilesMatch>
      
           ErrorLog ${APACHE_LOG_DIR}/site1.your_domain_error.log
           CustomLog ${APACHE_LOG_DIR}/site1.your_domain_access.log combined
      </VirtualHost>
      

      В этом файле вы изменили директорию на DocumentRoot, а ServerAdmin на адрес электронной почты, доступный администратору сайта your_domain. Также вы изменили параметр ServerName, устанавливающий базовый домен для этой конфигурации виртуального хоста, и добавили директиву SetHandler для запуска PHP как сервера процессов fastCGI.

      Сохраните и закройте файл.

      Теперь создайте новый файл конфигурации виртуального хоста для сайта site2.your_domain. Для этого субдомена мы будем развертывать php7.2:

      • sudo nano /etc/apache2/sites-available/site2.your_domain.conf

      Добавьте в файл следующее: Убедитесь, что путь к директории сайта, имя сервера и версия PHP соответствуют уникальным параметрам вашей системы:

      /etc/apache2/sites-available/site2.your_domain.conf

      <VirtualHost *:80>
           ServerAdmin admin@site2.your_domain
           ServerName site2.your_domain
           DocumentRoot /var/www/site2.your_domain
           DirectoryIndex info.php  
      
           <Directory /var/www/site2.your_domain>
              Options Indexes FollowSymLinks MultiViews
              AllowOverride All
              Order allow,deny
              allow from all
           </Directory>
      
          <FilesMatch .php$>
            # For Apache version 2.4.10 and above, use SetHandler to run PHP as a fastCGI process server
            SetHandler "proxy:unix:/run/php/php7.2-fpm.sock|fcgi://localhost"
          </FilesMatch>
      
           ErrorLog ${APACHE_LOG_DIR}/site2.your_domain_error.log
           CustomLog ${APACHE_LOG_DIR}/site2.your_domain_access.log combined
      </VirtualHost>
      

      Сохраните файл и закройте его после завершения. Проверьте файл конфигурации Apache на наличие синтаксических ошибок:

      • sudo apachectl configtest

      Вывод должен выглядеть так:

      Output

      Syntax OK

      Активируйте оба файла конфигурации виртуального хоста:

      • sudo a2ensite site1.your_domain
      • sudo a2ensite site2.your_domain

      Отключите сайт по умолчанию, поскольку он не потребуется:

      • sudo a2dissite 000-default.conf

      Перезапустите службу Apache, чтобы применить изменения:

      • sudo systemctl restart apache2

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

      Шаг 4 — Тестирование сайтов

      Мы настроили два сайта для работы с двумя разными версиями PHP. Теперь проверим результаты.

      Откройте в браузере сайты http://site1.your_domain и http://site2.your_domain. Вы увидите две страницы, выглядящие следующим образом:

      Информационная страница PHP 7.0Информационная страница PHP 7.2

      Обратите внимание на заголовки. На первой странице указано, что на сайте site1.your_domain развернута версия PHP 7.0. На второй странице указано, что на сайте site2.your_domain развернута версия PHP 7.2.

      Мы протестировали сайты и теперь можем удалить файлы info.php. Эти файлы представляют собой угрозу безопасности, поскольку они содержат важную информацию о вашем сервере и при этом доступны неуполномоченным пользователям. Чтобы удалить оба файла, запустите следующие команды:

      • sudo rm -rf /var/www/site1.your_domain/info.php
      • sudo rm -rf /var/www/site2.your_domain/info.php

      Теперь у вас имеется один сервер Ubuntu 18.04, обслуживающий два сайта с двумя разными версиями PHP. Однако PHP-FPM можно применять и для других целей.

      Заключение

      Мы объединили виртуальные хосты и PHP-FPM для обслуживания нескольких сайтов и нескольких версий PHP на одном сервере. Количество сайтов PHP и версий PHP, которые может обслуживать ваш сервер Apache, зависит исключительно от вычислительной мощности сервера.

      Теперь вы можете начать изучение более сложных функций PHP-FPM, таких как процесс адаптивного создания или функции регистрации sdtout и stderr. Также вы можете заняться защитой своих сайтов. Для этого используйте наше обучающее руководство по защите сайтов с помощью бесплатных сертификатов TLS/SSL от Let’s Encrypt.



      Source link

      Тестирование модуля Node.js с использованием Mocha и Assert


      Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Тестирование является неотъемлемой частью разработки программного обеспечения. Обычно программисты запускают код, который тестирует разработанные ими приложения, при внесении каких-либо изменений, чтобы убедиться, что все работает, как надо. При правильных тестовых настройках этот процесс можно автоматизировать, что значительно позволит сэкономить время. Запуск тестов непосредственно после написания нового кода гарантирует сохранность ранее существовавших функций. Таким образом разработчик может быть уверенным в базе кода, особенно когда она внедряется в производственную среду, чтобы пользователи могли взаимодействовать с ней.

      Мы создаем примеры тестирования с помощью структур тестовых фреймворков. Mocha — это популярный тестовый фреймворк JavaScript, используемый для организации и запуска тестовых файлов. Однако Mocha не подтверждает поведение нашего кода. Для сравнения значений в тесте мы можем использовать модуль Node.js assert​​​​​​.

      В этой статье вы узнаете, как написать тесты для списка дел (TODO) для модуля Node.js. Для создания тестов будет настроен и использован фреймворк Mocha. Также будет использован модуль Node.js assert для создания самих тестов. В этом смысле вы будете использовать Mocha в качестве планировщика, а assert​​​ для реализации плана.

      Предварительные требования

      • Node.js, установленный на вашем компьютере для разработки. В этом обучающем руководстве используется версия Node.js 10.16.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04.
      • Базовые знания JavaScript, которые можно получить из нашей серии статей Программирование на JavaScript.

      Шаг 1 — Создание модуля Node

      Давайте начнем с написания модуля Node.js, который мы будем тестировать. Этот модуль будет управлять списком элементов TODO. Используя этот модуль, мы сможем перечислить все элементы списка TODO, которые нужно отследить, добавить новые элементы и отметить некоторые как выполненные. Также мы сможем экспортировать список элементов TODO в файл CSV. Если вам нужно вспомнить, как писать модули Node.js, прочтите нашу статью Создание модуля Node.js.

      Для начала необходимо настроить среду программирования. Создайте папку с именем проекта в своем терминале. В данном обучающем руководстве будет использоваться имя todos:

      Затем откройте эту папку:

      Теперь инициализируйте npm, поскольку позже мы будем использовать его функцию командной строки для запуска тестирования:

      У нас есть только одна зависимость, Mocha, которую мы будем использовать для организации и запуска тестов. Для загрузки и установки Mocha воспользуйтесь следующей командой:

      • npm i request --save-dev mocha

      Мы установим Mocha как зависимость dev, поскольку это не требуется модулем в производственных настройках. Если вы хотите узнать больше о пакетах Node.js или npm, ознакомьтесь с руководством Использование модулей Node.js с npm и package.json.

      Наконец, создадим файл, который будет содержать код нашего модуля:

      Теперь мы готовы создать наш модуль. Откройте index.js​​​ в текстовом редакторе, например nano:

      Давайте начнем с определения класса Todos. Этот класс содержит все функции, необходимые для управления нашим списком TODO. Добавьте следующие строки кода в index.js:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      }
      
      module.exports = Todos;
      

      Начнем с создания класса Todos. Его функция constructor() не принимает аргументов, поэтому нам не нужно предоставлять значения для создания объекта для данного класса. Все, что мы делаем, когда инициализируем объект Todos, — это создаем свойство todos, которое является пустым массивом.

      Линия модулей позволяет другим модулям Node.js требовать наш класс Todos. Без прямого экспорта класса тестовый файл, который мы создадим позже, не сможет использовать его.

      Давайте добавим функцию для возврата сохраненного массива todos. Запишите следующие выделенные строки:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Функция list() возвращает копию массива, используемого классом. Она делает копию массива, используя деструктурирующий синтаксис JavaScript. Мы создаем копию массива, чтобы изменения, которые пользователь вносит в массив, возвращенный функцией list(), не влияли на массив, используемый объектом Todos.

      Примечание. Массивы JavaScript — это справочные файлы. Это значит, что для любого присваивания переменной для массива или вызова функции с массивом в качестве параметра JavaScript обращается к оригинальному созданному массиву. Например, если у нас есть массив с тремя элементами с именем x и мы создаем новую переменную y, так что y = x, y и x относятся к одному и тому же. Все изменения, выполняемые для массива с y, влияют на переменную x и наоборот.

      Теперь создадим функцию add(), которая добавляет новый элемент TODO:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      }
      
      module.exports = Todos;
      

      Наша функция add() берет строку и помещает ее в свойство title нового объекта JavaScript. Новый объект также имеет свойство completed, которое по умолчанию устанавливается на false. Затем мы добавляем этот новый объект к нашему массиву TODO.

      Важной функцией в менеджере TODO является отметка элементов как завершенные. Для выполнения этой задачи мы пройдем в цикле по нашему массиву todos, чтобы найти элемент TODO, который ищет пользователь. Если элемент найден, отметим его как завершенный. Если ничего не найдено, выдадим ошибку.

      Добавьте функцию complete()​​​ следующим образом:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      
          complete(title) {
              let todoFound = false;
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      }
      
      module.exports = Todos;
      

      Сохраните файл и выйдите из текстового редактора.

      Теперь у нас есть базовый менеджер TODO, с которым можно экспериментировать. Далее проверим код вручную, чтобы убедиться в работе приложения.

      Шаг 2 — Ручное тестирование кода

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

      Добавим в наше приложение два элемента TODO и отметим один из них как завершенный. Запустите Node.js REPL в той же папке, что и файл index.js:

      Вы увидите командную строку > в REPL, которая указывает, что мы можем ввести код JavaScript. Введите в командную строку следующее:

      • const Todos = require('./index');

      С помощью require() мы загружаем модуль TODO в переменную Todos. Помните, что наш модуль возвращает класс Todos по умолчанию.

      Теперь инстанцируем объект для этого класса. В REPL добавьте следующую строку кода:

      • const todos = new Todos();

      Мы можем использовать объект todos для проверки работы реализации. Добавим первый элемент TODO:

      До сих пор мы не видели никаких выводов в нашем терминале. Давайте убедимся, что мы сохранили элемент TODO run code, получив список всех наших TODO:

      Вы увидите следующий вывод в вашем REPL:

      Output

      [ { title: 'run code', completed: false } ]

      Это ожидаемый результат: у нас есть один элемент TODO в нашем массиве TODO, и он не завершен по умолчанию.

      Добавим другой элемент TODO:

      • todos.add("test everything");

      Отметим первый элемент TODO как завершенный:

      • todos.complete("run code");

      Теперь наш объект todos будет управлять двумя элементами: run code и test everything. TODO run code также будет завершен. Подтвердим это, вызвав list()​​​ еще раз:

      Вывод REPL будет выглядеть следующим образом:

      Output

      [ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

      Теперь закройте REPL следующим образом:

      Мы подтвердили, что наш модуль работает соответствующим образом. Хотя мы не поместили наш код в тестовый файл и не использовали тестовую библиотеку, мы вручную протестировали код. К сожалению, эта форма тестирования займет много времени, если ее использовать при выполнении каждого изменения. Далее попробуем выполнить автоматизированное тестирование в Node.js и посмотрим, возможно ли решить данную проблему с помощью тестового фреймворка Mocha.

      Шаг 3 — Создание первого теста с помощью Mocha и Assert

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

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

      В этом обучающем руководстве мы используем тестовый фреймворк Mocha с модулем Node.js assert​​​. Давайте на практике посмотрим, как они вместе работают.

      Для начала создадим новый файл для хранения кода теста:

      Теперь с помощью предпочтительного текстового редактора откройте файл тестирования. Можно использовать nano, как раньше:

      В первой строке текстового файла мы загрузим модуль TODO аналогично тому, как мы делали в оболочке Node.js. Затем мы загрузим модуль assert​​​, чтобы он был на момент создания тестов. Добавьте следующие строки:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      

      Свойство strict​​​​ модуля assert позволит нам использовать специальные тесты эквивалентности, рекомендуемые Node.js, которые также подходят для проверок в дальнейшем, поскольку отвечают за большее число вариантов использования.

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

      describe([String with Test Group Name], function() {
          it([String with Test Name], function() {
              [Test Code]
          });
      });
      

      Обратите внимание на две ключевые функции: describe() и it()​​​. Функция describe() используется для группировки аналогичных тестов. Для Mocha не требуется запускать тесты, но их группировка упростит поддержку нашего кода теста. Рекомендуется группировать тесты таким образом, чтобы было проще обновлять аналогичные вместе.

      it() содержит наш код теста. Именно здесь мы могли бы взаимодействовать с функциями нашего модуля и использовать библиотеку assert​​. Многие функции it() могут быть определены в функции describe().

      Цель этого раздела состоит в использовании Mocha и assert для автоматизации нашего ручного теста. Мы будем делать это постепенно, начав с блока описания. Добавьте в файл следующее после строк модуля:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

      С помощью этого блока кода мы создали группировку для наших объединенных тестов. Тесты блока проверяют по одной функции за раз. Интеграционные тесты проверяют, насколько хорошо функции в модулях или между ними работают вместе. Когда Mocha запускает наш тест, все тесты в этом блоке описания будут запущены в группе интеграционных тестов.

      Давайте добавим функцию it(), чтобы начать тестирование нашего кода модуля:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
          });
      });
      

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

      Для нашего первого теста мы создадим новый объект Todos и проверим, что в нем нет элементов:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Первая новая строка кода инстанциировала новый объект Todos, как мы делали в Node.js REPL или другом модуле. Во второй новой строке мы использовали модуль assert​​​.

      Из модуля assert мы используем метод notStrictEqual()​​​. Эта функция учитывает два параметра: значение, которое необходимо протестировать (называется фактическое значение), и значение, которое мы ожидаем получить (называется ожидаемое значение). Если эти оба аргумента одинаковы, notStrictEqual()​​​ выдает ошибку о непрохождении теста.

      Сохраните и закройте index.test.js.

      Базовый сценарий будет истинным, так как длина должна быть 0, что не равно 1. Давайте убедимся в этом, запустив Mocha. Для этого нам потребуется модифицировать наш файл package.json. Откройте файл package.json в своем текстовом редакторе:

      Теперь в свойстве scripts измените его следующим образом:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Мы только что изменили поведение команды test командной строки npm. Когда мы запустим npm test, npm проверит команду, которую мы только что ввели в package.json. Он будет искать библиотеку Mocha в нашей папке node_modules​​​ и запустит команду mocha с нашим файлом тестирования.

      Сохраните и закройте package.json.

      Давайте посмотрим, что происходит, когда мы запускаем наш тест. В своем терминале введите:

      Команда выдаст следующий вывод:

      Output

      > [email protected] test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Этот вывод сначала покажет нам, какая группа тестов сейчас запустится. Для каждого отдельного теста в группе тестовый сценарий является ступенчатым. Мы видим наше имя теста так, как мы описали его в функции it(). Галочка с левой стороны тестового сценария указывает на то, что тест пройден.

      Внизу мы получим резюме всех наших тестов. В нашем случае один тест был выполнен и завершен в течение 16 мс (время зависит от компьютера).

      Тестирование началось успешно. Однако текущий тестовый сценарий может допускать ложные позитивные результаты. Ложные позитивные результаты — это тестовый сценарий, когда тест пройден тогда, когда не должен.

      Теперь мы проверяем, что длина массива не равна 1. Давайте изменим тест, чтобы это условие было истинным, когда не должно. Добавьте следующие строки в index.test.js​​​:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Сохраните и закройте файл.

      Мы добавили два элемента TODO. Давайте запустим тест, чтобы увидеть, что произойдет:

      В результате вы получите следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

      Он проходит согласно ожиданиям, так как длина больше 1. Однако он не достигает первоначальной цели проведения этого первого теста. Первый тест должен был подтвердить, что мы начинаем с чистого состояния. Более совершенный тест подтвердит это во всех случаях.

      Давайте изменим тест таким образом, что его успешное прохождение будет возможным только при полном отсутствии TODO в памяти. Выполните следующие изменения в index.test.js​​:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Вы изменили notStrictEqual()​​​​​​ на strictEqual()​​​, функцию, которая проверяет эквивалентность между фактическим и ожидаемым аргументом. Строгое равенство (Strict equal) завершится неудачей, если наши аргументы не полностью одинаковы.

      Сохраните и закройте файл, затем запустите тест, чтобы увидеть, что произойдет:

      В этот раз вывод покажет ошибку:

      Output

      ... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

      Этот текст пригодится только для отладки причины непрохождения теста. Обратите внимание, что поскольку тест не был пройден, в начале тестового сценария не было галочки.

      Резюме теста находится уже не внизу вывода, а сразу после нашего списка отображенных тестовых сценариев:

      ...
      0 passing (29ms)
        1 failing
      ...
      

      В остальной части вывода предоставлены данные о непройденных тестах. Сначала мы видим, какие тестовые сценарии не пройдены:

      ...
      1) integrated test
             should be able to add and complete TODOs:
      ...
      

      Затем мы увидим причину непрохождения теста:

      ...
            AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
      + expected - actual
      
      - 2
      + 0
            + expected - actual
      
            -2
            +0
      
            at Context.<anonymous> (index.test.js:9:10)
      ...
      

      Выдается AssertionError, когда не выполняется strictEqual()​​​. Мы видим, что ожидаемое значение 0 отличается от фактического значения 2.

      Затем мы увидим строку в нашем файле тестирования, где код не выполняется. В этом случае это строка 10.

      Теперь мы воочию убедились, что наш тест не будет пройден, если мы будем ожидать некорректные результаты. Давайте изменим наш тестовый сценарий обратно на правильное значение. Откройте файл:

      Затем выберите строки todos.add, чтобы ваш код выглядел следующим образом:

      todos/index.test.js

      ...
      describe("integration test", function () {
          it("should be able to add and complete TODOs", function () {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Сохраните и закройте файл.

      Запустите его еще раз, чтобы убедиться в прохождении без каких-либо ложных позитивных результатов:

      Вывод будет выглядеть следующим образом:

      Output

      ... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

      Теперь мы значительно улучшили отказоустойчивость нашего теста. Давайте перейдем к нашему интеграционному тесту. Следующий шаг — добавить новый элемент TODO в index.test.js​​​:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
          });
      });
      

      После использования функции add() мы подтверждаем, что у нас есть один элемент TODO, управляемый нашим объектом todos, при этом мы будем использовать strictEqual()​​. Наш следующий тест подтверждает данные в todos с помощью deepStrictEqual(). Функция deepStrictEqual() рекурсивно проверяет, имеют ли наши предполагаемые и реальные объекты одни и те же свойства. В этом случае проверяется, содержат ли оба ожидаемых массива объект JavaScript. Затем проверяется, имеют ли эти объекты JavaScript одинаковые свойства, т. е. оба их свойства title — это run code, а оба свойства completedfalse.

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

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
      
              todos.add("test everything");
              assert.strictEqual(todos.list().length, 2);
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: false },
                      { title: "test everything", completed: false }
                  ]
              );
      
              todos.complete("run code");
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: true },
                      { title: "test everything", completed: false }
                  ]
          );
        });
      });
      

      Сохраните и закройте файл.

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

      Давайте еще раз запустим тест npm test для получения данного вывода:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

      Вы настроили комплексный тест с помощью Mocha и библиотеки assert.

      Рассмотрим ситуацию, когда мы разделили наш модуль с другими разработчиками, и теперь они предоставляют нам обратную связь. Большинство пользователей хотели бы, чтобы функция complete() возвращала ошибку, в случае если ни один элемент TODO еще не добавлен. Добавим это свойство в функцию complete().

      Откройте index.js в редакторе:

      Добавьте в функцию следующее:

      todos/index.js

      ...
      complete(title) {
          if (this.todos.length === 0) {
              throw new Error("You have no TODOs stored. Why don't you add one first?");
          }
      
          let todoFound = false
          this.todos.forEach((todo) => {
              if (todo.title === title) {
                  todo.completed = true;
                  todoFound = true;
                  return;
              }
          });
      
          if (!todoFound) {
              throw new Error(`No TODO was found with the title: "${title}"`);
          }
      }
      ...
      

      Сохраните и закройте файл.

      Теперь добавим новый тест для этой новой функции. Нам нужно убедиться, что в случае если мы вызываем команду complete объекту Todos, в котором нет элементов, будет возвращена ошибка.

      Вернитесь в index.test.js​​:

      В конце файла добавьте следующий код:

      todos/index.test.js

      ...
      describe("complete()", function() {
          it("should fail if there are no TODOs", function() {
              let todos = new Todos();
              const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
      
              assert.throws(() => {
                  todos.complete("doesn't exist");
              }, expectedError);
          });
      });
      

      Снова используем describe() и it()​​. Этот тест начинается с создания нового объекта todos. Затем мы определяем ошибку, которую ожидаем получить при вызове функции complete().

      Далее используем функцию throws() модуля assert. Эта функция была создана для проверки ошибок, которые выдаются в коде. Первый аргумент — это функция, содержащая код, который выдает ошибку. Второй аргумент — это ошибка, которую мы ожидаем получить.

      Снова запустите тест с помощью npm test​​​ в своем терминале и вы увидите следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

      Этот вывод подтверждает преимущества автоматизированного тестирования с помощью Mocha и assert. Поскольку наши тесты выполняются скриптами, каждый раз, когда мы запускаем npm test, мы проверяем, что все тесты успешно пройдены. Нам не нужно вручную проверять, работает ли другой код. Мы знаем, что работает, так как наш тест успешно пройден.

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

      Шаг 4 — Тестирование асинхронного кода

      Одна из функций, описанных в нашем модуле TODO, — это функция экспорта CSV. Она выводит все элементы TODO, а также завершенный статус в файл. Для этого требуется использовать модуль fs — встроенный модуль Node.js для работы с файловой системой.

      Запись в файл — это асинхронная операция. В Node.js есть много способов записи в файл. Можно использовать обратные вызовы, обещания или ключевые слова async/await. В этом разделе мы рассмотрим, как записывать тесты для разных методов.

      Обратные вызовы

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

      Добавим функцию в наш класс Todos с именем saveToFile(). Эта функция будет создавать строку, проходя циклом через все элементы TODO и записывая эту строку в файл.

      Откройте файл index.js:

      Добавьте в этот файл следующий выделенный код:

      todos/index.js

      const fs = require('fs');
      
      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
              this.todos.push(todo);
          }
      
          complete(title) {
              if (this.todos.length === 0) {
                  throw new Error("You have no TODOs stored. Why don't you add one first?");
              }
      
              let todoFound = false
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      
          saveToFile(callback) {
              let fileContents = 'Title,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Сначала необходимо импортировать модуль fs в наш файл. Затем добавляем новую функцию saveToFile()​​. Эта функция выполняет функцию обратного вызова, которая активируется сразу после завершения операции записи файла. В этой функции мы создаем переменную fileContents, содержащую всю строку, которую мы хотим сохранить в качестве файла. Она активируется с помощью заголовков CSV. Затем проходим циклом через каждый элемент TODO с помощью метода внутреннего массива forEach(). В процессе итерации добавляем свойства title и completed отдельных объектов todos.

      Наконец, используем модуль fs для записи файла с помощью функции writeFile(). Первый аргумент — это имя файла: todos.csv. Второй — это содержимое файла, в этом случае fileContents — это переменная. Последний аргумент — это наша функция обратного вызова, которая обрабатывает любые ошибки записи файла.

      Сохраните и закройте файл.

      Теперь напишем тест для функции saveToFile(). Этот тест выполняет две функции: в первую очередь подтверждает наличие файла, а затем проверяет, имеет ли файл правильное содержимое.

      Откройте файл index.test.js:

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

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      const fs = require('fs');
      ...
      

      Теперь в конце файла добавим новый тест:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function(done) {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.saveToFile((err) => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Как и ранее, используем команду describe() для группировки нашего теста отдельно от других, так как он подразумевает новую функцию. Функция it() несколько отличается от других функций. Обычно у используемой нами функции обратного вызова нет аргументов. В этот раз у нас есть done в качестве аргумента. Этот аргумент требуется при тестировании функций с обратными вызовами. Функция обратного вызова done() используется Mocha для информирования о завершении асинхронной функции.

      Все функции обратного вызова, протестированные в Mocha, должны вызывать обратный вызов done(). Если нет, Mocha не будет знать, когда функция была завершена, и зависнет в ожидании сигнала.

      Далее создаем экземпляр Todos и добавляем в него один элемент. Затем вызываем функцию saveToFile()​​​ с обратным вызовом, который фиксирует ошибку записи файла. Обратите внимание, как тест для этой функции располагается в обратном вызове. Если бы код теста был за пределами обратного вызова, тест бы не прошел, так как код вызывался до завершения записи файла.

      В нашей функции обратного вызова мы сначала проверяем наличие нашего файла:

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      Функция fs.existsSync() возвращает true, если путь файла в аргументе существует, и false, если нет.

      Примечание. Функции модуля fs — асинхронные по умолчанию. Однако для ключевых функций существуют синхронные копии. Этот тест упрощен с помощью синхронных функций, так как нам не нужно встраивать асинхронный код для проверки работы теста. В модуле fs синхронные функции, как правило, имеют Sync ​​​в конце имен.

      Затем создаем переменную для хранения ожидаемого значения:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Используем readFileSync() модуля fs для синхронного чтения файла:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Теперь предоставляем readFileSync() правильный путь для файла: todos.csv​​. Поскольку readFileSync() возвращает буферный объект Buffer, который хранит бинарные данные, мы используем метод toString() для сравнения его значения со строкой, которую мы предположительно сохранили.

      Как и ранее, используем strictEqual модуля assert для выполнения сравнения:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Заканчиваем тест вызовом обратного вызова done()​​​, чтобы убедиться, что Mocha знает, что нужно остановить тестирование:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Мы указываем объект err в done(), тогда Mocha не пройдет тест в случае возникновения ошибки.

      Сохраните и закройте index.test.js.

      Запускаем этот тест с помощью npm test, как и ранее. Вы увидите следующий вывод на консоли:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

      Вы протестировали первую асинхронную функцию с Mocha, используя функцию обратных вызовов. Но, как описывается в статье Написание асинхронного кода в Node.js, на момент написания этого обучающего руководства обещания используются чаще, чем обратные вызовы в новом коде Node.js. Далее давайте посмотрим, как протестировать их с помощью Mocha.

      Обещания

      Обещание — это объект JavaScript, который в конечном счете возвращает значение. Когда обещание успешно, оно разрешено. Когда встречается ошибка, оно отклоняется.

      Давайте изменим функцию saveToFile()​​ таким образом, чтобы она использовала обещания вместо обратных вызовов. Откройте index.js​​:

      Сначала нам нужно изменить загрузку модуля fs. В вашем файле index.js измените выражение require() в верхней части файла, чтобы это выглядело следующим образом:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Мы только что импортировали модуль fs, который использует обещания, а не обратные вызовы. Теперь нам нужно внести некоторые изменения в команду saveToFile(), чтобы она работала с обещаниями.

      В вашем текстовом редакторе внесите в функцию saveToFile() следующие изменения для удаления обратных вызовов:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

      Первое отличие — это тот факт, что наша функция больше не принимает никакие аргументы. В случае с обещаниями нам не нужна функция обратного вызова. Второе отличие касается того, как написан файл. Теперь мы возвращаем результат обещания writeFile().

      Сохраните и закройте index.js.

      Теперь давайте изменим наш тест так, чтобы он работал с обещаниями. Откройте index.test.js​​:

      Замените тест saveToFile()​​​ на следующее:

      todos/index.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function() {
              let todos = new Todos();
              todos.add("save a CSV");
              return todos.saveToFile().then(() => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      Первое, что нужно изменить, — это удалить обратный вызов done() из аргументов. Если Mocha передает аргумент done(), его необходимо вызвать или он выдаст ошибку такого типа:

      1) saveToFile()
             should save a single TODO:
           Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
            at listOnTimeout (internal/timers.js:536:17)
            at processTimers (internal/timers.js:480:7)
      

      При тестировании обещаний не включайте обратный вызов done() в it().

      Для проверки обещания нам нужно задать код утверждения в функцию then(). Обратите внимание, что мы возвращаем это обещание в тест, и у нас нет функции catch() для перехвата при отклонении обещания.

      Мы возвращаем обещание, чтобы любые ошибки, выданные в функции then(), всплыли в функции it(). Если ошибки не всплывают, Mocha не провалит тест. При тестировании обещаний вам нужно использовать return для тестируемого обещания. Если нет, вы рискуете получить ложный позитивный результат.

      Также мы пропускаем выражение catch(), так как Mocha может обнаружить, когда обещание отклоняется. При отклонении тест автоматически проваливается.

      Теперь, когда у нас есть тест, сохраните и закройте файл, затем запустите Mocha с npm test для подтверждения, что мы получим успешный результат:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

      Мы изменили наш код и тест для использования обещаний, и теперь мы точно знаем, что это работает. Но последние асинхронные модели используют ключевые слова async/await, поэтому нам не нужно создавать множественные функции then() для обработки успешных результатов. Давайте посмотрим, как работает тест с async/await.

      async/await

      Ключевые слова async/await делают работу с обещаниями менее многословной. Когда мы определяем функцию как асинхронную с ключевым словом async, мы можем получить любые дальнейшие результаты в этой функции с ключевым словом await. Так мы можем использовать обещания без необходимости использования функций then() или catch().

      Можно упростить наш тест saveToFile(), который основан на обещании с async/await. В вашем текстовом редакторе создайте эти незначительные изменения к тесту saveToFile() в index.test.js:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", async function() {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Первое изменение — это тот факт, что функция, используемая it(), имеет ключевое слово async, когда она определена. Это позволяет использовать ключевое слово await в ее теле.

      Второе изменение обнаруживается, когда мы вызываем saveToFile(). Ключевое слово await используется перед вызовом. Теперь Node.js знает, что нужно ждать, пока эта функция не решится перед продолжением теста.

      Код функции проще читать, когда мы переместили код, который был в функции then() в тело функции it(). Запуск этого кода с помощью npm test дает следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

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

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

      Шаг 5 — Использование хуков для улучшения тестовых случаев

      Хуки — полезный элемент Mocha, который позволяет нам настроить среду до и после теста. Обычно мы добавляем хуки в блок функции describe(), так как они содержат логику установки и сноса, специфичную для некоторых тестовых случаев.

      Mocha предоставляет четыре типа хуков, которые используются в тестах:

      • before: этот хук запускается один раз перед началом первого теста.
      • beforeEach: этот хук запускается перед каждым тестовым случаем.
      • after: этот хук запускается один раз после завершения последнего тестового случая.
      • afterEach: этот хук запускается после каждого тестового случая.

      Когда мы тестируем функцию или свойство несколько раз, хуки очень помогают, так как они позволяют нам отделять код установки теста (например создание объекта todos) от кода утверждения теста.

      Для просмотра значения хуков добавим больше тестов в наш блок теста saveToFile().

      Хотя мы подтвердили, что можем сохранить элементы списка TODO в файл, мы сохранили только один элемент. Более того, элемент не был помечен как завершенный. Добавим больше тестов, чтобы убедиться, что различные аспекты нашего модуля работают.

      Сначала добавим второй тест для подтверждения того, что наш файл сохранен корректно, после завершения элемента списка TODO. Откройте файл index.test.js в своем текстовом редакторе:

      Измените последний тест на следующее:

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          it("should save a single TODO", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.complete("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Тест аналогичен тому, что мы делали ранее. Основное отличие в том, что мы вызываем функцию complete() перед вызовом saveToFile() и что ожидаемые элементы файла expectedFileContents​​​ теперь имеют значение true вместо false для столбца completed.

      Сохраните и закройте файл.

      Запустим наш новый тест, а также все остальные с помощью npm test​​​:

      В результате вы получите следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

      Он работает, как и ожидалось. Но возможности для улучшения все еще есть. Они оба должны инстанцировать объект Todos в начале теста. По мере того как мы добавляем больше тестовых случаев, это быстро становится повторяющимся и отнимает память. Также каждый раз, когда мы запускаем тест, создается файл. Кто-то менее знакомый с модулем может ошибочно принять это за реальный вывод. Было бы неплохо очистить наши файлы вывода после тестирования.

      Сделаем эти улучшения с помощью тестовых хуков. Мы используем хук beforeEach() для настройки тестовой конфигурации элементов TODO. Тестовая конфигурация — это любое последовательное состояние, используемое в тесте. В нашем случае тестовая конфигурация — это новый объект todos, в который уже добавлен один элемент TODO. Затем мы используем afterEach() для удаления файла, созданного тестом.

      В index.test.js внесите следующие изменения в ваш последний тест для saveToFile():

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          beforeEach(function () {
              this.todos = new Todos();
              this.todos.add("save a CSV");
          });
      
          afterEach(function () {
              if (fs.existsSync("todos.csv")) {
                  fs.unlinkSync("todos.csv");
              }
          });
      
          it("should save a single TODO without error", async function () {
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync("todos.csv"), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              this.todos.complete("save a CSV");
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Давайте разберем все изменения, которые мы внесли. Мы добавили блок beforeEach() в тестовый блок:

      todos/index.test.js

      ...
      beforeEach(function () {
          this.todos = new Todos();
          this.todos.add("save a CSV");
      });
      ...
      

      Эти две строки кода создают новый объект Todos, доступный для каждого нашего теста. С Mocha объект this в beforeEach() относится к такому же объекту this в it(). this одинаков для каждого блока кода внутри блока describe(). Подробнее о this ищите в нашем обучающем руководстве Понимание методов This, Bind, Call и Apply в JavaScript​​​.

      Благодаря мощному обмену контекстом мы можем быстро создавать тестовые конфигурации, которые подходят для обоих тестов.

      Затем мы очищаем наш файл CSV в функции afterEach():

      todos/index.test.js

      ...
      afterEach(function () {
          if (fs.existsSync("todos.csv")) {
              fs.unlinkSync("todos.csv");
          }
      });
      ...
      

      Если тест не прошел, то он не мог создать файл. Поэтому мы проверяем наличие файла перед использованием функции unlinkSync() для его удаления.

      Остальные изменения переключают ссылку с объектов todos, созданных ранее в функции it()​​​, на this.todos, имеющиеся в контексте Mocha. Также мы удалили строки, которые ранее инстанциировали todos в отдельных тестовых случаях.

      Теперь запустим этот файл, чтобы убедиться, что тест все еще работает. Введите в терминале npm test​​​, чтобы получить следующее:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

      Результаты те же, и к тому же мы немного сократили время установки для новых тестов для функции saveToFile() и нашли решение для остаточного файла CSV.

      Заключение

      В этом обучающем руководстве мы написали модуль Node.js для управления элементами TODO и протестировали код вручную с помощью Node.js REPL. Затем создали тестовый файл и использовали фреймворк Mocha для запуска автоматизированных тестов. С помощью модуля assert мы смогли проверить корректность работы кода. Также протестировали синхронные и асинхронные функции с Mocha. Наконец, создали хуки с Mocha, которые делают написание связанных тестовых случаев более читабельным и управляемым.

      С помощью этих знаний попробуйте самостоятельно написать тесты для новых модулей Node.js, созданных вами. Вы можете подумать о вводах и выводах вашей функции и написать тест до написания кода?

      Если вы хотите узнать больше о тестовом фреймворке Mocha, ознакомьтесь с официальной документацией Mocha. Если вы хотите продолжить изучение Node.js, вы можете перейти к странице серии Написание кода на Node.js.



      Source link