vak: (Default)
[personal profile] vak
Навеяло недавним достижением [personal profile] spamsink: восстановлением исходников игры "Посадка на Луну" с БЭСМ-6.

https://github.com/besm6/bega-re/blob/master/landing.pas

Пусть у нас есть диалоговое приложение на Си++ с текстовым интерфейсом. Приложение запрашивает построчный ввод из std::cin и выдаёт результаты на std::cout. Условно говоря, вызов выглядит следующим образом:
application(std::cin, std::cout);
Схематично, работает оно так:
void application(std::istream &input, std::ostream &output)
{
output << "something\n";

while (!input.eof()) {
std::string line;
getline(input, line);

std::string result = something(line);
output << result;
}
}
Теперь стоит задача покрыть имеющуюся реализацию тестами. Я ж теперь адепт TDD. Хочется иметь возможность делать тесты (с применением Catch) вида:
send("foo\n");
std::string reply = receive();
REQUIRE(reply == "bar\n");
Как бы вы стали решать эту задачу? Я имею в виду, как средствами Си++ привинтить одно к другому? Чтобы, не модифицируя исходники application(), вызывать и тестировать её из Catch?

Нет, это не задача для собеседования. Тут и опытный человек голову сломает.

Традиционно такие штуки принято делать через Expect, но выглядит такой подход слишком громоздко. Лучше найти решение средствами самого языка Си++.

Date: 2020-08-12 23:41 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

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

И чо?

Date: 2020-08-13 00:04 (UTC)
spamsink: (Default)
From: [personal profile] spamsink
Ну есть же разнообразные stringstreams, причём им можно буфера на ходу переключать. Мы этим регулярно пользуемся.

Date: 2020-08-13 00:46 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
application() в данном случае - это REPL.
В Unit-тестах надо вызывать внутренние функции something().

А если речь идёт о тестировании целого приложения, то вызвать его в отдельном процессе с редиректом STDIN, STDOUT в Pipe.
И в тестах вызывать что-то вроде:
Verify(pipeOut, pipeIn, "some input", "expected output").


Date: 2020-08-13 01:26 (UTC)
From: [personal profile] ex0_planet
Как уже справедливо заметили, это не юнит-тесты. Это, в лучшем случае, — интеграционные. Для юнит тестов мокается всё что возможно, после этого вызывается минимально возможный черный ящик, который и хотим протестировать.

Date: 2020-08-13 02:03 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Так это, из строки у вас там нельзя стрим сделать, что ли? Это как-то уж больно было бы странно. Это если мы можем вызвать функцию. А нет - так через ОС вызвать эту хрень и приклеить stdio и stdout. На джаве элементарно.

Date: 2020-08-13 04:36 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

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

Date: 2020-08-13 05:09 (UTC)
spamsink: (Default)
From: [personal profile] spamsink
Не для тестирования. У нас, например, используется фальшивый streambuf, который ничего не буферизует, а только SHA считает - ради инкрементной компиляции. В твоем случае можно сделать класс над streambuf, в котором будет вся умность, и дать его обоим стримам.

Date: 2020-08-13 05:30 (UTC)
spamsink: (Default)
From: [personal profile] spamsink
Я в данном случае не вижу необходимости в async. Общий streambuf для двух стримов оказывается моделью внешнего мира; получается эдакая инверсия контроля.

Date: 2020-08-13 07:03 (UTC)
norian: (Default)
From: [personal profile] norian
коты для небольших проектов юзают тестовые классы для юнитов

внизу :

class EgLocationTests
{
public:

EgDataNodesType testDataNodes;

bool testCreateLocations();

bool testAddLocations();

bool testLoadLocationsData();

void testShowResult(bool res, const QString &theMessage)
{
if (res)
{
qDebug().noquote() << "PASS" << theMessage;
}
else
{
qDebug().noquote() << "FAILED" << theMessage;
}
}
};

наверху:

tmpRes = attrTests.testCreateAttributes();
res = res && tmpRes;

tmpRes = attrTests.testAddAttributes();
res = res && tmpRes;

tmpRes = attrTests.testLoadAttributes();
res = res && tmpRes;

if (res)
qDebug() << "\nAll tests PASSED\n";
else
qDebug() << "\nSome tests FAILED\n";

дальше можно запускать или лапгами или каким-нть скриптом и парсить на "FAIL"

бтв, а во что оборачивать чтобы сорцы на дриме нормально показывались ?
Edited Date: 2020-08-13 07:04 (UTC)

Тоже мне бином Ньютона

Date: 2020-08-13 07:27 (UTC)
spamsink: (Default)
From: [personal profile] spamsink
#include <iostream>
#include <streambuf>
#include <queue>

class Feedback : public std::streambuf {
    std::queue<char> q;
    char c;
public:

    Feedback() : std::streambuf() {
        setp(0, 0);
        setg(0, 0, 0);
        for (auto c : "1 1 ")
            if (c) q.push(c);
    }
    int underflow() {
        c = q.front();
        q.pop();
        setg(& c, & c, & c+1);
        return c;
    }
    int overflow(int c) {
        std::cout << char(c);
        q.push(c);
        return 0;
    }
};

void fib(std::istream & in, std::ostream & out) {
    int a, b;
    in >> a;
    for (int i = 0; i < 20; ++i) {
        in >> b;
        out << a + b << ' ';
        a = b;
    }
    out << '\n';
}

int main() {
    Feedback fb;
    std::istream in(&fb);
    std::ostream out(&fb);
    fib(in, out);
}
Edited (краткость) Date: 2020-08-13 07:40 (UTC)

Date: 2020-08-13 07:28 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
Если средствами C++, то надо написать свою реализацию std::streambuf
и инициализировать ей std::istream, std::ostream с которыми вызовете application()

P.S. sorry, увеидел что уже ниже написали
Edited Date: 2020-08-13 07:29 (UTC)

Date: 2020-08-13 17:16 (UTC)
From: [personal profile] caztd
Здесь решение для google mock:
https://stackoverflow.com/questions/36119525/google-mock-mock-fstream-object-and-control-of-execution-in-test

Наверняка можно аналогично и для Catch смастерить.

Date: 2020-08-13 21:54 (UTC)
norian: (Default)
From: [personal profile] norian
коты для небольших проектов юзают тестовые классы для юнитов

внизу :

class EgLocationTests
{
public:

    EgDataNodesType testDataNodes;

    bool testCreateLocations();

    bool testAddLocations();

    bool testLoadLocationsData();

    void testShowResult(bool res, const QString &theMessage)
    {
        if (res)
        {
            qDebug().noquote() << "PASS" << theMessage;
        }
        else
        {
            qDebug().noquote() << "FAILED" << theMessage;
        }
    }
};


наверху:

    tmpRes = attrTests.testCreateAttributes();
    res = res && tmpRes;

    tmpRes = attrTests.testAddAttributes();
    res = res && tmpRes;

    tmpRes = attrTests.testLoadAttributes();
    res = res && tmpRes;

    if (res)
        qDebug() << "\nAll tests PASSED\n";
    else
        qDebug() << "\nSome tests FAILED\n";


дальше можно запускать или лапгами или каким-нть скриптом и парсить на "FAIL"
Edited Date: 2020-08-13 21:55 (UTC)

Date: 2020-08-13 21:54 (UTC)
norian: (Default)
From: [personal profile] norian
ага, тхенкс, так лучше видно

Date: 2020-08-13 22:00 (UTC)
From: [personal profile] caztd
Нужно же что-то вроде?
MyStreamMockClass mock;
MyAppClass app(mock); 
EXPECT_CALL(mock, GetInputString()).WillOnce(Return("input"));
std::string reply = app.receive();


Конечно вам нужен свой stream-interface wrapper так как ifstream.get() не виртуальный метод.

Re: Красиво

Date: 2020-08-13 22:13 (UTC)
spamsink: (Default)
From: [personal profile] spamsink
Если ты хочешь в точности в таком виде, то придется запускать приложение в отдельном треде, да. Но с минимальными отличиями этого можно избежать:
Interact world;
world.send("foo\n");
application(std::istream(& world), std::ostream(& world));
std::string reply = world.receive();
REQUIRE(reply == "bar\n");


Date: 2020-08-13 22:23 (UTC)
From: [personal profile] caztd
Какая разница. Вызывайте функцию.
Mock может запомнить список ожидаемых значений.
Не совсем REPL, но для теста достаточно.
Потом можно проверить весь output сразу или тоже сделать мок и задать список ожидаемых строк -- если вам построчно нужно.

Или вам нужно менять ввод в зависимости от вывода?

Date: 2020-08-13 22:52 (UTC)
From: [personal profile] caztd
Ну просто редко кто такую логику в unit test вставляет.
Если это тестовое приложение, тогда моки не помогут,
нужeн pipe или streambuf, как предложили выше.
Что-то типа такого:
https://stackoverflow.com/questions/28356629/connecting-two-streaming-functions-c

Re: Красиво

Date: 2020-08-13 23:24 (UTC)
spamsink: (Default)
From: [personal profile] spamsink
Вот объект world класса Interact и будет этим динамически заниматься после запуска application. Если угодно, внутри этого класса send эквивалентен вставке аргумента в очередь "ввода", а receive - доставанию результата из очереди "вывода". Из моего примера это должно вытекать.

Date: 2020-08-14 09:40 (UTC)
norian: (Default)
From: [personal profile] norian
йа бы использовал адаптер - функция получает потоки как аргументы, а более верхний уровень либо назначает стандартные, либо подставляет кастомные для тестирования

а, ну собсно оно так уже написано (на третий день коты посмотрели на топик, ага), просто дёргать снаружи со своими стримами и сравнивать результаты с ожидаемыми
Edited Date: 2020-08-14 09:46 (UTC)

Date: 2020-08-14 13:33 (UTC)
norian: (Default)
From: [personal profile] norian
как-нть так

#include iostream
#include sstream

using namespace std;

void application( istream& input,  ostream &output)
{
    output << "Start" << endl;

    ostringstream local_str;
    local_str << input.rdbuf();

    output << local_str.str() + "_test";
}

int main() {

    stringstream in_str;
    stringstream out_str;

    in_str << "Hello";

    application( in_str, out_str );

    if (out_str.str() == "Start\nHello_test")
        cout << "PASS" << endl;
    else
        cout << "FAIL " << endl;

    return 0;
}



с функцией из топика тоже будет работать, только шаблон результата поменять на фактический
и третья строка не нужна, лишний копипаст был
Edited Date: 2020-08-14 14:15 (UTC)

Re: application(std::cin, std::cout); - ut

Date: 2020-08-15 17:30 (UTC)
dememax: (скука)
From: [personal profile] dememax
> Я имею в виду, как средствами Си++ привинтить одно к другому? Чтобы, не модифицируя исходники application(), вызывать и тестировать её из Catch?
Лучше найти решение средствами самого языка Си++.


Вот, хоть режьте меня, но я не понимаю, зачем в этом случае Си++.
Если я правильно понял, тестировать надо, что при определённом вводе есть определённый вывод.

> Традиционно такие штуки принято делать через Expect, но выглядит такой подход слишком громоздко.

А не боимся, что с Си++ может быть ещё более громоздко?

Re: основной софт на Си++

Date: 2020-08-15 20:14 (UTC)
dememax: (вэлком)
From: [personal profile] dememax
:-)


Система конфигурации сбрки - тоже на Си++?
И система сборки - на Си++?
Логично же, нет?!
;-)

Тогда и
void next_step(const std::string & sec_name, 
        const std::string & cmd,
        const std::string & answer)
{
    SECTION(sec_name) {
        session.send(cmd + "\n");
        reply = session.receive();
        REQUIRE(reply == answer + "\n");
    }
}
неплохо бы ввести.