Aquesta sessió també es fa sense Jutge! El treball és personal i es pot fer localment en el teu ordinador.
En aquesta sessió de laboratori veurem la classe Stack i la classe Queue, però abans aprendrem una forma d'assegurar-nos que els nostres programes fan el que esperem. Provar programes és costós, però la bona notícia és que amb la mateixa programació ho podem automatitzar, i per tant és típic utilitzar algun mecanisme per poder testar que un programa, una funció o una classe fan el que volem.
La primera part tracta sobre com es fa servir Doctest, a la segona farem les classes Stack i Queue, i a la tercera, problemes que fan servir piles i cues. En cap moment ens caldrà el Jutge perquè nosaltres mateixos farem el paper del Jutge!
Doctest és una llibreria per testar programes en C++ molt potent. Descarrega-la primer:
Recorda treballar en una carpeta nova i obrir aquesta carpeta per
treballar amb VSCode!
No obris una carpeta amb tots els teus fitxers!
Ara tindràs el fitxer doctest.h en una carpeta, per si sol. Per
escriure tests amb doctest.h, s'ha de fer un programa com el
següent:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include <string>
using namespace std;
TEST_CASE("primer test") {
int a = 1;
CHECK(a == 1);
}
() {
string s = ;
(s.() == );
}
Bàsicament:
Cal posar l'#include "doctest.h" (compte amb el .h, no és
.hh), afegint el #define ... primer, que fa que quan
compilem el programa, tingui un main sense haver-lo de fer
nosaltres.
Cada test és com una "funció" TEST_CASE, amb el nom del test
entre parèntesis, i després un bloc de codi que fa el test. El
codi farà certes operacions i per comprovar que els resultats
són els bons, es crida CHECK, amb un valor que hauria de
donar cert.
Copia el codi anterior en un fitxer test_prova.cc, compila'l i
executa'l.
Hauries de veure una cosa com:
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
==========================================================
[doctest] test cases: 2 | 2 passed | 0 failed | 0 skipped
[doctest] assertions: 2 | 2 passed | 0 failed |
[doctest] Status: SUCCESS!
Racional amb DoctestEl test_prova.cc era per provar una mica Doctest, però ara anem
a fer-lo servir de veritat. Descarrega el ZIP següent:
Crea un directori nou i posa-hi els dos fitxers racional.hh i
racional.cc a dins. Copia o descarrega de nou doctest.h i
posa'l juntament amb els altres.
Veuràs que la classe Racional és la solució de l'exercici de la
primera sessió de laboratori. Però per poder testar hauràs de
fer un nou mètode. Es tracta d'un mètode per comparar dos
racionals. Ara mateix, no podem fer això:
Racional r1, r2;
CHECK(r1 == r2); // ERROR: no tenim "==" per Racionals :(
Afegeix un mètode igual_que a la classe Racional de manera
que poguem escriure tests així:
Racional r1, r2(0, 1);
CHECK(r1.igual_que(r2));
Així, doncs, escriu tests senzills per poder comprovar que les
operacions aritmètiques amb Racionals funcionen, incloent la
comparació:
TEST_CASE("comparacio") {
Racional a, b(0, 1);
CHECK(a.igual_que(b));
Racional c(1,5), d(2, 10);
CHECK(c.igual_que(d));
}
De fet, sabem que hi ha algun bug a la implementació de
Racional (algunes IAs són un desastre... 🙄), així que és
possible que alguns tests funcionin i d'altres no. Assegura't
primer que els tests estan comprovant coses que realment
saps que són certes! Si no, els tests t'estaran enganyant.
Compila i executa el programa i mira la sortida per veure quins
tests van bé i quins no.
Quan un test falla, apareix una cosa com:
TEST CASE: resta
test_racional.cc:36: ERROR: CHECK( ... ) is NOT correct!
Si fas servir el terminal de dins de VSCode, pots prémer Ctrl i
clicar test_racional.cc:36: i aniràs a parar al punt exacte del
codi que ha fallat!
Amb la informació que tens, mira ara d'arreglar els errors i tornar a executar els tests. Hi ha un truc que consisteix en executar la compilació i l'execució en una sola comanda al terminal:
g++ -o test_racional *.cc ; ./test_racional
El ; permet posar dues comandes seguides. Però té un problema,
què passa si la compilació dóna errors? Doncs que s'executarà el
test igualment, amb la versió antiga! Hi ha una manera d'arreglar
això:
g++ -o test_racional *.cc && ./test_racional
Igual que amb les expressions Booleanes en C++, si separes les
comandes del terminal amb && la segona no s'executa si la
primera no ha anat bé!
Stack<T> i Queue<T>En aquesta secció treballarem amb Stack<T> i Queue<T> i
completarem els seus tests. També farem tests que comproven que
quan es crida un mètode, aquest produeixi un error, com passa amb
la pila si fem pop quan està buida.
Stack<T>Descarrega el directori amb el projecte de Stack:
Descomprimeix el ZIP en un directori i fes make. Veuràs que
molts tests no tenen cap CHECK i que l'últim produeix un error
sense ni tan sols tenir CHECK.
Completa els tests de la classe pila posant CHECKs per
comprovar que els mètodes funcionen bé. Ves fent make per
veure com progresses.
Per arreglar el test anomenat "Pop en pila buida", el que cal
fer és cridar una variant de CHECK que es diu
CHECK_THROWS_WITH, que es crida així:
CHECK_THROWS_WITH(pila.pop(), "Cagada pastoret!");
El que fa aquest CHECK és realment comprovar que si cridem
pop amb una pila buida, dóna un error, i que l'error
retorna un missatge concret (que no és "Cagada pastoret!").
Posa el CHECK_THROWS_WITH per aquest cas amb els paràmetres
que calguin. També busca un altre assert dins de la pila
que es dispara en una condició diferent i que necessita un
test per comprovar que també funciona.
Queue<T>Amb la cua hem de fer semblant, cal descarregar el ZIP,
descomprimir, i acabar els tests de la cua, incloent els que
produeixen un error per l'assert:
istream, ostream i stringstreamPer poder practicar els problemes d'aquesta secció, necessitem saber coses noves sobre els streams de C++, que ens seran molt útils quan fem tests de programes que llegeixen o escriuen.
istream: un canal d'entrada qualsevolD'entrada, cin és un objecte, i la seva classe és istream
(una abreviació de "input stream"). Malgrat ara només
coneixem cin, hi ha més canals d'entrada i tots ells es
comporten igual (tots tenen l'operador >> per poder llegir
enters, caràcters, etc.).
Això ens permet fer una funció com:
#include <iostream>
using namespace std;
int llegeix_enter(istream& in) {
int x;
in >> x;
return x;
}
int main() {
int a = llegeix_enter(cin);
}
Aquest programa és interessant perquè té una funció
llegeix_enter que no llegeix de cin, sinó d'un canal
qualsevol, que és un objecte que es passa com a paràmetre. És a
dir, podem fer que llegeix_enter llegeixi d'on vulguem, de fet.
El nom de la variable és in però podria ser qualsevol altre, el
que passa és que in resulta més similar a cin, perquè
suggereix "input".
ostream: un canal de sortida qualsevolUna cosa semblant passa amb ostream. Quan volem escriure, podem
fer servir qualsevol objecte de classe ostream i podrem fer el
mateix que fem amb cout:
void escriu_dos_cops(ostream& out, string p) {
out << p << ' ' << p;
}
Per cridar aquesta funció es podria fer:
escriu_dos_cops(cout, "Bora");
i torna a passar com abans, estem passant cout com a paràmetre
de la funció però podríem passar una altra cosa.
istringstream i ostringstream: canals que fan servir cadenes de caràctersPer entendre la utilitat de istringstream i ostringstream,
fem primer un programa que faci servir alhora l'entrada i la
sortida:
void suma_sequencia_enters(istream& in, ostream& out) {
int suma = 0, x;
while (in >> x) {
suma += x;
}
out << suma << endl;
}
La funció suma_sequencia_enters podria ser un main i fer
servir cin i cout, però fent-ho així, podrem provar el
programa passant-li una entrada que prové d'un string, i
recollir la sortida també en un string.
L'entrada serà un istringstream, o sigui un canal d'entrada
construit a partir d'un string:
istringstream sin("1 2 3 4 5");
Per fer servir aquesta classe cal fer #include <sstream>, i la
declaració anterior, clarament crida un constructor que crea un
objecte sin (que és un istream), a partir d'un text. El nom
és expressament semblant a cin, però amb una s perquè fa
servir strings per dins.
Ara podem crear el canal de sortida, que no omplirem nosaltres
(sinó el programa suma_sequencia_enters):
ostringstream sout;
Amb els dos canals preparats, es pot invocar el programa anterior així:
suma_sequencia_enters(sin, sout);
que s'executarà i anirà llegint els caràcters de sin, que
efectivament són una seqüència d'enters, i finalment escriurà
sobre sout, que teòricament emmagatzemarà la sortida.
Només ens cal recollir la sortida amb el mètode str i mirar-la:
// el mètode 'str' retorna el contingut de `sout`
string resultat = sout.str();
Tot plegat, si volem escriure un test per a la funció
suma_sequencia_enters, podem fer-ho així:
TEST_CASE("suma curta") {
istringstream sin("1 2 3 4 5");
ostringstream sout;
suma_sequencia_enters(sin, sout);
CHECK(sout.str() == "15\n"); // '\n' és perquè hi ha un 'endl'!!
}
En funció del sistema operatiu, és possible que el \n sigui
un \r\n (Windows).
Els següents problemes tots tenen una estructura que fa servir la tècnica explicada a la secció anterior per testar les entrades i sortides de certs problemes, però el codi del problema està per fer. O sigui, que descarregant cada ZIP tindràs una petita carpeta amb un problema "tipus Jutge", però en què tots els tests són visibles i controlats per tu mateix.