Setting up E2E Tests in Gitlab CI: Laravel + React + Cypress
Tags: devops, laravel, react, testing
Published: 2022-02-09 07:14:21

This is an overview of how we got our e2e cypress tests running for our Laravel backend, and React frontend. 🚀

> Even if your stack is identical to ours, your environment may still slightly differ depending on versions, and providers. This post is only meant to serve as a guide, and not as a copy-paste solution.

**Sections**

- `.gitlab-ci.yml` - Gitlab CI config
- API Docker Image - Laravel app

**TL;DR, Create a single Docker image for your Laravel App + webserver, and use it as a Gitlab [service](https://docs.gitlab.com/ee/ci/services/)**

#### Motivation

I've spent the last 2 days struggling to make this happen. Through a series of small undocumented wins, we finally reached that sweet ✅ pipeline. Seeing as I've had to piece together information from various sources, and travel back in time, I thought I'd summarize my findings here in case it helps anyone with a similar stack.

### **.gitlab-ci.yml**

```yml
E2E Local Tests:
  image: cypress/browsers:node16.13.0-chrome95-ff94
  services:
  - mysql:5.7
  - name: registry.gitlab.com/your_project/api_ci:latest
    alias: api
  variables:
    # Create separate network, required for services to talk to each other
    # Reference: https://docs.gitlab.com/ee/ci/services/#connecting-services
    FF_NETWORK_PER_BUILD: 1
    MYSQL_DATABASE: myapp
    MYSQL_ROOT_PASSWORD: secret
    DB_USERNAME: root
    DB_DATABASE: myapp
    DB_PASSWORD: secret
    DB_HOST: mysql
    REACT_APP_API_URL: http://api:8000
  script:
    # Verify the api is up, and running (optional)
    - curl http://api:8000
    # Install npm packages, and start server in background
    - npm install
    - npm run start&
    - sleep 120 # Wait for server to be up
    - npm run e2e:local # run tests
```

- Set `FF_NETWORK_PER_BUILD` to tell Gitlab to create a netowrk for our job. This is required if you want your services to talk to each other, ie., api, and db, which we do.
- **registry.gitlab.com/your_project/api_ci:latest** would be your API Laravel app, bundled in a single docker image with your web-server. In our case it was our app + nginx (with php-fpm).
- Aliased the API service to `api`
- Make sure you set the `DB_HOST` to the service name as well, in this case `mysql`.
- Tell React app the API url is now `http://api:8000`, where `api` is the alias we gave our service above.
- Start react app with `&` to run in background.

### API Docker Image - Laravel app

To get the API up, and running, 2 solutions came to mind:

1. Use `git` to pull in the project, and setup volume mounts + nginx service
2. Bundle everything together in a single Docker image

**Option 2.** is definitely the simpler option, so that's the one we went with.

#### Dockerfile

```
# Build
FROM php:7.4-fpm as build

# Install PHP dependencies to get Laravel up, and running
RUN apt-get update && apt-get install -y \
    git \
    curl \
    libpng-dev \
    libonig-dev \
    libxml2-dev \
    zip \
    unzip \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libzip-dev \
    cron \
    openssh-client

# Install xdebug for code coverage
RUN pecl install xdebug \
  && docker-php-ext-enable xdebug

# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/
RUN docker-php-ext-install -j$(nproc) gd pdo_mysql zip bcmath pcntl

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Where php-fpm expects the project files to live
WORKDIR /var/www

# FPM default user (www-data) must own the files,
# or we'll hit a permission error in Laravel
RUN chown -R www-data:www-data /var/www

# Include a docker-init.sh (optional)
COPY ./Docker/dev/docker-init.sh /usr/local/bin/docker-init.sh
RUN chmod +x /usr/local/bin/docker-init.sh

# Bake our entire project into the image
COPY . .


# Copy configs

COPY supervisord.conf /etc/supervisord.conf
COPY nginx-site.conf /etc/nginx/conf.d/default.conf

# Copy start script
COPY start.sh /start.sh
RUN chmod 755 /start.sh

# IMPORTANT - need to tell gitlab which port to check, otherwise it will timeout at 'waiting at services'
EXPOSE 8000

ENTRYPOINT [ "docker-init.sh" ]

CMD ["/start.sh"]
```

- Installed nginx & supervisor to run the app.
- Copied all files into the image.
- `docker-init.sh` custom script that does prep stuff like run migrations, and seeders.
- `start.sh` is the final docker command, in this case we're starting supervisor

#### Bonus: `docker-init.sh`

Here's where we init the app.

```
#!/bin/sh

# Exit if any fails
set -e

# Install dependencies / upgrade packages
composer install

# Clear cache/config to make sure env is read
php artisan route:clear
php artisan config:clear

# Migrate
php artisan migrate:refresh --seed

php artisan storage:link

# execute default entrypoint
docker-php-entrypoint $@
```

Next we have our start script, which is really only just starting supervisor.

#### `start.sh`

```
#!/bin/bash
# Start supervisord and services
exec /usr/bin/supervisord -n -c /etc/supervisord.conf
```

#### Build & Run

Once you've got your `Dockerfile` ready, build, and push it to your private Gitlab repository.

> Note that the image name has to be the full URL of the repository.

### Other Issues

#### App reading wrong ENV

If you find your API not reading the ENV set in either `.gitlab-ci.yaml` or the Dockerfile, it's because `php-fpm` aren't reading those values. Most likely because it was started by supervisor. 

The fix is to make sure you use the same values in a `.env` file.