По данным npm, каждую неделю neo4j-driver скачивают около 40 000 человек.



Но, к моему удивлению, когда два месяца назад я попытался спросить в Neo4j Online Community лучший способ имитировать вызовы сеанса драйвера Neo4j, никто не предложил конкретный инструмент или общий подход.

Использовать стандартный серверный mocker для neo4j-driver не так-то просто, потому что запросы используют объект session, сгенерированный драйвером.

Вы всегда можете заглушить вызов функции, но получить правильный результат сложно. Проблема в том, что результат запроса включает массив из Record объектов. Типичной функциональностью вашей программы будет запуск метода get() для Records в вашем выводе. Этот метод существует только в том случае, если вы просматриваете Result, созданный драйвером neo4j.

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

Моя основная предпосылка заключается в том, что любой должен иметь возможность имитировать вызовы сервера всех типов. Я решил, что кто-то из сообщества Neo4j должен что-то сделать.

Поэтому я построил neo-forgery, чтобы имитировать сеансы Neo4j.

Я в восторге от этого ... Это действительно хорошо работает! Теперь пользователи Neo4j могут пользоваться преимуществами TDD и полным тестовым покрытием в своих узловых приложениях.

Пример использования Neo-Forgery

README для нео-подделки говорит вам, что вам нужно делать, но позвольте мне показать вам быстрый пример. Это руководство предполагает базовые, но минимальные знания узлов, Neo4j и модульного тестирования.

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

Многое из того, что ниже, не имеет прямого отношения к нео-подделке, но я хочу, чтобы вы четко понимали, как был построен код.

Я буду использовать TypeScript, который добавляет несколько шагов, но показывает, как использовать экспортированные типы в новой подделке. Конечно, вы всегда можете использовать JS и игнорировать набор текста. Я буду использовать AVA как тестовый раннер.

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

Вот шаги, которые мы предпримем:

  1. Создайте пустой проект TypeScript
  2. Создайте запрос Neo4j в браузере данных
  3. Сохранить запрос
  4. Сохраните ожидаемый ответ на запрос
  5. Создать тест
  6. Запустить тест
  7. Создайте функцию с помощью TDD

1. Создайте пустой проект TypeScript.

Следующие четыре шага полезны для создания проекта TypeScript и начала тестирования с использованием AVA.

(1) Запустите эти команды в терминале, чтобы создать проект с средством запуска тестов AVA с использованием TypeScript:

mkdir movieBuff
cd movieBuff
npm init -y
npm init ava
npm install --save-dev typescript ts-node

(2) Добавьте эту спецификацию AVA в свой package.json, чтобы использовать AVA с машинописным текстом и указать тестовый каталог с файлами для тестирования:

"ava": {
  "files": [
    "test/**/*.test.ts"
  ],
  "extensions": [
    "ts"
  ],
  "require": [
    "ts-node/register"
  ]
},

(3) Создайте каталоги src и test и добавьте начальный тестовый файл: test/sample.test.ts:

import test from 'ava';

const fn = () => 'foo';

test('fn() returns foo', t => {
	t.is(fn(), 'foo');
});

Теперь вы можете открыть терминал и вызвать npm test в каталоге проекта, чтобы убедиться, что он запущен. Вы должны увидеть что-то вроде этого:

Затем удалите test/sample.test.ts. Скоро тебе предстоит настоящее испытание.

(4) Добавьте файл tsconfig.json со следующим содержимым, чтобы включить определенные функции, когда мы начнем кодирование:

{
  "compilerOptions": {
    "declaration": true,
    "importHelpers": true,
    "module": "commonjs",
    "outDir": "lib",
    "rootDirs": ["./src", "./test"],
    "strict": true,
    "target": "es2017"
  },
  "include": [
    "src/**/*"
  ]
}

2. Создайте запрос Neo4j

Мы воспользуемся базой данных примеров фильмов. Вы можете создать свой собственный экземпляр базы данных фильмов, войдя в Neo4j Sandbox.

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

Затем настройте запрос и образец параметра. Я просто приведу вам один для этого примера.

Сначала создайте параметр, введя :param title => 'Hoffa'. Должно получиться так:

Затем запустите запрос, в котором используется параметр:
match (m:Movie {title:$title}) return m. Вы должны увидеть один результат:

3. Сохраните запрос.

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

Сохраните его в новом файле filmQuery.ts со следующим содержимым:

export const filmQuery = `
match (m:Movie {title:$title}) return m
`

Перейдите в каталог вашего проекта в терминале и запустите его, чтобы установить neo4j-драйвер:

npm i neo4j-driver

Теперь создайте файл src/filmInfo.ts с пустой функцией, которая вызывает запрос:

import {Session} from "neo4j-driver";
export async function getFilm(title: string, session: Session) {
}

Мы создадим тест, ожидая неудачи, еще до того, как создадим функцию.

4. Сохраните ожидаемый ответ на запрос.

А теперь самое интересное - использование нео-подделки для создания теста, имитирующего реальный вызов запроса.

Сначала установите neo-forgery:

npm i -D neo-forgery

На всякий случай создайте подкаталог test/data. Затем создайте файл-заполнитель test/data/expectedOutput.ts:

import {MockOutput} from "neo-forgery";
export const expectedOutput: MockOutput = {
    records: []
}

Затем вернитесь к запросу в браузере данных. Вы можете нажать Code слева, а затем Response:

Откроется поле ответа, которое вы можете выделить и скопировать:

Вставьте это в test/data/expectedOutput.ts, заменив [] фактическим ответом.

import {StoredResponse} from "neo-forgery";

export const expectedOutput: StoredResponse = {
    records: [
        {
            "keys": [
                "m"
            ],
            "length": 1,
            "_fields": [
                {
                    "identity": {
                        "low": 141,
                        "high": 0
                    },
                    "labels": [
                        "Movie"
                    ],
                    "properties": {
                        "louvain": {
                            "low": 142,
                            "high": 0
                        },
                        "degree": 5,
                        "tagline": "He didn't want law. He wanted justice.",
                        "title": "Hoffa",
                        "released": {
                            "low": 1992,
                            "high": 0
                        }
                    }
                }
            ],
            "_fieldLookup": {
                "m": 0
            }
        }
    ]
}

5. Создайте тест

Создайте тестовый файл test/filmInfo.test.ts. По сути, файл делает следующее:

  1. Создайте QuerySet с помощью одного запроса. Запрос будет использовать заданный вами ожидаемый вывод.
  2. Используйте mockSessionFromQuerySet , чтобы создать фиктивный сеанс из QuerySet.
  3. Проверьте утверждение, что ваша filmInfo функция возвращаетexpectedOutput при вызове с вашим запросом. Обратите внимание, что expectedOutput имеет тип StoredResponse, поэтому он должен быть преобразован вспомогательной функцией storedToLive для правильного сравнения с фактическим выводом.

Вот полный тестовый файл:

import test from 'ava'
import {
    mockSessionFromQuerySet, 
    storedToLive, 
    QuerySpec
} from 'neo-forgery'
import {filmInfo} from '../src/filmInfo'
const {filmQuery} = require('../filmQuery')
const {expectedOutput} = require('./data/expectedOutput')
const title = 'Hoffa'
const querySet: QuerySpec[] = [{
    name: 'requestByTitle',
    query: filmQuery,
    params: {title},
    output: expectedOutput
}]
test('mockSessionFromQuerySet returns correct output', async t => {
    const session = mockSessionFromQuerySet(querySet)
    const output = await filmInfo(title, session)
    t.deepEqual(output,storedToLive(expectedOutput))
})

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

6. Тестирование функции

Откройте терминал для тестирования и запустите npm test -- -w. Эта опция -w сообщает ava о необходимости непрерывного выполнения тестов.

При первом запуске ava вы должны увидеть что-то вроде этого:

$ cd $CURRENT && npm test -- -w
> [email protected] test
> ava "-w"
✖ No tests found in test/data/expectedOutput.ts, make sure to import "ava" at the top of your test file [10:47:23]
─
filmInfo.ts › mockSessionFromQuerySet returns correct output
test/filmInfo.test.ts:25
24:     const output = await filmInfo(title, session)                   
   25:     t.deepEqual(output,mockResultsFromCapturedOutput(expectedOutput…
   26: })
Difference:
- undefined
  + {
  +   records: [
  +     Record {
  +       _fieldLookup: Object { … },
  +       _fields: Array [ … ],
  +       keys: Array [ … ],
  +       length: 1,
  +       ---
  +       Object { … },
  +     },
  +   ],
  +   resultsSummary: {},
  + }
› test/filmInfo.test.ts:25:7
─
1 test failed

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

7. Создайте функцию

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

Если вам нужно научиться работать с результатами neo4j, вы можете ознакомиться с их документацией по API. Вот решение, которое подойдет:

import {Session} from "neo4j-driver";
import {filmQuery} from "./filmQuery";
export async function filmInfo(title: string, session: Session) {
    let returnValue: any = {}
    try {
        returnValue = await session.run(
            filmQuery,
            {
                title,
            })
    } catch (error) {
        throw new Error(`error getting film info: ${error}`)
    }
return returnValue
}

Ава дает нам мгновенное положительное подкрепление!

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

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

Щелчок по таблице в левом столбце отобразит в табличной форме возвращаемые записи:

Допустим, для нас released (назовем это year) и tagline составляют информацию о фильме. Сохраните следующее объявление интерфейса в FilmFacts.ts:

export interface FilmFacts {
    year: number;
    tagline: string;
}

Затем мы можем изменить тест, чтобы ожидать экземпляр FilmFacts. Содержимое экземпляра filmFacts можно получить прямо из браузера данных. Дополнения и модификации выделены жирным шрифтом ниже:

import {FilmFacts} from '../FilmFacts'
...
const filmFacts:FilmFacts = {
    tagline: "He didn't want law. He wanted justice.",
    year: 1992,
}
test('mockSessionFromQuerySet returns correct output', async t => {
    const session = mockSessionFromQuerySet(querySet)
    const output = await filmInfo(title, session)
    t.deepEqual(output,filmFacts)
})

ПРИМЕЧАНИЕ. Возможно, идеально было бы обновить запрос, чтобы он возвращал только те, но мы изменим наш код для этого примера.

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

import {Session} from "neo4j-driver";
import {filmQuery} from "./filmQuery";
import {FilmFacts} from "./FilmFacts";

export async function filmInfo(title: string, session: Session):
    Promise<FilmFacts> {
    let returnValue: any = {}
    try {
        const result = await session.run(
            filmQuery,
            {
                title,
            })

        const movieProperties =
            result.records[0].get('m').properties

        returnValue = {
            tagline: movieProperties.tagline,
            year: movieProperties.released.low
        }

    } catch (error) {
        throw new Error(`error getting film info: ${error}`)
    }

    return returnValue
}

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

Хотя это выходит за рамки данного руководства, должна быть предусмотрена обработка общей ошибки, связанной с отсутствием возвращаемых данных. См. Пример в файле filmInfo.ts в образце полного решения.

Короче говоря, мы создали и протестировали запрос neo4j, даже без вызова сеанса!

Завершить проект до точки, указанной в образце полного решения, нетривиально. Как правило, добавьте файл .env с учетными данными базы данных фильмов и файл index.ts, который вызывает настоящую базу данных с учетными данными. И вам нужно создать интерактивную функцию, которая запрашивает у пользователя название фильма и возвращает код. Для приличия добавил линтинг, тестовое покрытие и .gitignore.

Наслаждайтесь и открывайте вопрос с любыми проблемами или запросами! В оставшейся части этой статьи объясняется, как настроить ваш .run() метод в имитационном сеансе для более сложных тестов.

Пользовательский сеанс .run () Содержание

В приведенном выше примере используется mockSessionFromQuerySet.

Иногда вам могут понадобиться некоторые функции в вашем тесте, которые не покрываются сеансом, возвращаемым mockSessionFromQuerySet.

Проще всего это сделать, просто перезаписав run() для вашего фиктивного экземпляра сеанса. Вы даже можете изменить его на один тест. Например, в образце полного решения вы можете увидеть, что тест filmInfo содержит два случая, когда я перезаписываю метод сеанса run(), чтобы вернуть необходимую ошибку. См. Код, выделенный жирным шрифтом ниже:

function mockRunForTypeError(){
    const e = new Error('not found')
    e.name = 'TypeError'
    throw e
}

test('filmInfo NonExistent Film', async t => {
    const emptyFilmFacts: FilmFacts = {year: 0, tagline: ''}
    const session = mockSessionFromQuerySet(querySet)
    session.run = mockRunForTypeError
    const output = await filmInfo('nonExistent', session)
    t.deepEqual(output, emptyFilmFacts)
})

Вы также можете создать свой собственный run() с нуля, включая запросы обо всем, что вам нужно. Для этого используйте mockSessionFromFunction(sessionRunMock: Function) вместо mockSessionFromQuerySet.

Вы можете создать все, что захотите, в функции. Единственное предостережение: ваша функция должна возвращать массив Neo4j Records. С этой целью neo-forgery предлагает две полезные функции :

  1. dataToLive принимает массив объектов и преобразует их в записи neo4j.
  2. storedToLive возьмет захваченный результат запроса и преобразует его в записи neo4j.

Например:

const sampleRecordList = [ {
 “firstName”: Tom,
 “lastName”: Franklin,
 “id”: “2ea51c4a-c072–4797–9de7–4bec0fc11db3”,
 “email”: [email protected],
}, {
 “firstName”: Sarah,
 “lastName”: Jenkins,
 “id”: “2ea51c4a-c072–4797–9de7–4bec0fc11db3”,
 “email”: [email protected],
}]
const sessionRunMock = (query: string, params: any) => {
    return mockResultsFromData(sampleRecordList);
};
const session = storedToLive(sessionRunMock)

Резюме

Инструмент neo-forgery создает очень эффективную заглушку, которая имитирует настоящий сеанс neo4j-driver. Теперь вы можете создать код и протестировать его без дополнительных обращений к базе данных.



Это открывает пользователям Neo4j дверь для быстрой разработки через тестирование.