A **TDD (Test-Driven Development)** és a **BDD (Behavior-Driven Development)** két népszerű fejlesztési módszer, amelyek tesztelési folyamatokra építenek, de eltérő szemlélettel és célkitűzésekkel.
Fő különbségek:
==== 1. Fókusz ====
* **TDD (Test-Driven Development):**
* A TDD középpontjában a **kód implementációja** áll. A cél az, hogy a fejlesztő **előre megírja a teszteket** a kód implementálása előtt, majd a tesztek alapján hozza létre a funkciókat. A TDD alacsonyabb szintű tesztekre (pl. unit tesztekre) összpontosít, amelyek konkrét kódrészleteket vizsgálnak.
* A TDD lépései:
* 1. Írj egy tesztet (amely először el fog bukni).
* 2. Írd meg a kódot, hogy a teszt sikeres legyen.
* 3. Refaktoráld a kódot, ha szükséges.
* **BDD (Behavior-Driven Development):**
* A BDD a **viselkedésre** összpontosít, azaz arra, hogy a rendszernek hogyan kell viselkednie a felhasználó szempontjából. A tesztek a rendszer viselkedését írják le, nem pedig a kód részleteit. A BDD teszteket természetes nyelv közeli, mindenki által érthető formában írják meg, gyakran felhasználva a Gherkin szintaxist (`Given`, `When`, `Then` struktúrában).
* A BDD célja az üzleti elemzők, fejlesztők és tesztelők közötti **együttműködés elősegítése**, hogy minden érintett jobban megértse a rendszer elvárt viselkedését.
==== 2. Szint ====
* **TDD:**
* Főként alacsony szintű (unit tesztek) tesztelésre összpontosít. A tesztek a kódrészletek helyes működését ellenőrzik.
* **BDD:**
* Magasabb szintű tesztelés, amely a rendszer viselkedését vizsgálja, például hogyan reagál bizonyos felhasználói interakciókra vagy üzleti folyamatokra.
==== 3. Szemléletmód ====
* **TDD:**
* A kódtervezés **teszt-alapú**. A fejlesztő először tesztet ír, majd ehhez igazítja a kódot. A TDD során a fejlesztők inkább a funkciók implementálására és a kód helyességére koncentrálnak.
* **BDD:**
* A tervezés **viselkedés-alapú**. A tesztek a rendszer által elvárt viselkedést írják le, tehát a felhasználói élményt és üzleti igényeket helyezik előtérbe.
==== 4. Nyelvezet ====
* **TDD:**
* Teszteket gyakran programozási nyelveken írják meg, amelyeket elsősorban a fejlesztők értenek. Például egy TDD teszt Pythonban, JUnitban stb. íródik.
* **BDD:**
* A teszteket emberi nyelven közeli módon fogalmazzák meg, így nemcsak fejlesztők, hanem üzleti elemzők és más érintettek is megértik. A Gherkin szintaxis egy példa erre:
Given the user is on the login page
When they enter valid credentials
Then they should be logged in successfully
===== Példa =====
A nulláról indulunk, lépésenként bemutatjuk a módszert.
==== 1. Projekt inicializálása ====
Először hozzunk létre egy új projekt könyvtárat, és inicializáld a Node.js projektet.
mkdir tdd-project
cd tdd-project
npm init -y
Ez létrehoz egy alap **package.json** fájlt.
==== 2. Függőségek telepítése ====
Telepítsük a szükséges fejlesztői függőségeket: **Mocha** a teszteléshez, **Chai** az asszertálásokhoz, és **Sinon** a mockoláshoz és stuboláshoz. Mivel a projektben jelszó hash-elésre is szükség lesz, telepítük a **bcrypt** könyvtárat is.
npm install mocha chai sinon bcrypt --save-dev
==== 3. Mappastruktúra létrehozása ====
Hozzuk létre a szükséges mappákat és fájlokat a projekt szerkezetéhez.
mkdir test
mkdir services
mkdir repositories
ni ./test/userService.test.js
ni ./services/userService.js
ni ./repositories/userRepository.js
//megjegyzés//: az ''ni'' parancs powershell-ben a linuxos ''touch'' parancs megfelelője.
Most a projekt struktúrája így néz majd ki:
tdd-project/
│
├── test/
│ └── userService.test.js // Tesztek a UserService-hez
│
├── services/
│ └── userService.js // UserService osztály
│
├── repositories/
│ └── userRepository.js // UserRepository osztály
│
└── package.json // Node.js projekt leíró fájl
Létrejött három üres állomány.
==== 4. Mocha konfigurálása ====
A **Mocha** futtatásához a ''package.json'' fájlban hozzá kell adni egy részt, amely a ''mocha'' parancsot futtatja a ''test'' mappában:
Nyissuk meg a ''package.json'' fájlt, és addjuk hozzá a ''scripts'' részhez a következőt:
"type": "module",
"scripts": {
"test": "mocha"
}
==== 5. Tesztek írása (TDD módszerrel) ====
Most kezdhetjük a TDD folyamatot: először a teszteket írdjuk meg. Például a ''userRepository.test.js'' fájlba írjuk a következő teszteket:
import assert from 'assert';
import UserRepository from '../repositories/userRepository.js';
describe('UserRepository', function() {
let userRepository;
beforeEach(function() {
userRepository = new UserRepository();
});
it('should return null if user is not found by email', async function() {
const user = await userRepository.findUserByEmail('notfound@example.com');
assert.strictEqual(user, null);
});
it('should save a user and retrieve it by email', async function() {
const newUser = { email: 'test@example.com', password: 'hashedPassword' };
await userRepository.saveUser(newUser);
const foundUser = await userRepository.findUserByEmail('test@example.com');
assert.strictEqual(foundUser.email, 'test@example.com');
assert.strictEqual(foundUser.password, 'hashedPassword');
});
it('should handle saving multiple users', async function() {
const user1 = { email: 'user1@example.com', password: 'password1' };
const user2 = { email: 'user2@example.com', password: 'password2' };
await userRepository.saveUser(user1);
await userRepository.saveUser(user2);
const foundUser1 = await userRepository.findUserByEmail('user1@example.com');
const foundUser2 = await userRepository.findUserByEmail('user2@example.com');
assert.strictEqual(foundUser1.email, 'user1@example.com');
assert.strictEqual(foundUser2.email, 'user2@example.com');
});
});
==== 6. Tesztek futtatása ====
Futtasd a Mocha teszteket, hogy megbizonyosodj arról, hogy a tesztek elbuknak (mivel még nem írtad meg a tényleges implementációt).
npm test
Ez a parancs futtatja a ''mocha'' parancsot, amely végigmegy a ''test'' mappában lévő teszteken. Mivel a ''UserRepository'' még nem implementált, a tesztek elbuknak, ami a TDD módszer lényege: először a tesztek buknak el, majd az implementáció következik.
{{:tanszek:oktatas:pasted:20241012-113656.png}}
==== 7. Implementáció megírása ====
Most írd meg a tényleges kódot a tesztek sikeressé tételéhez.
''userRepository.js'':
// repositories/userRepository.js
class UserRepository {
constructor() {
this.users = []; // Szimulált adatbázis tömbként
}
// Felhasználó keresése e-mail alapján
async findUserByEmail(email) {
const user = this.users.find(user => user.email === email);
return user || null;
}
// Új felhasználó mentése az adatbázisba
async saveUser(user) {
this.users.push(user);
return user;
}
}
export default UserRepository;
==== 8. Tesztek újrafuttatása ====
Most futtassuk újra a teszteket:
npm test
Most a teszteknek sikeresen át kell menniük, mivel az implementáció megfelel a tesztek elvárásainak.
{{:tanszek:oktatas:pasted:20241012-113731.png}}
==== 9. BDD stílusú tesztek ====
Hozzuk létre a ''test/userRepository.test.js''-t az alábbi tartalommal.
import { expect } from 'chai';
import sinon from 'sinon';
import bcrypt from 'bcrypt';
import UserRepository from '../repositories/userRepository.js';
import UserService from '../services/userService.js';
describe('UserService', function() {
let userService;
let userRepositoryStub;
beforeEach(function() {
// Mockoljuk az adatbázis hívásokat
userRepositoryStub = sinon.stub(UserRepository.prototype, 'findUserByEmail');
userService = new UserService(new UserRepository());
});
afterEach(function() {
// Restore minden stubolt funkciót a tesztek után
sinon.restore();
});
it('should return an error if the email is already in use', async function() {
// Szimuláljuk, hogy az email már létezik
userRepositoryStub.resolves({ email: 'existing@example.com' });
const result = await userService.registerUser('existing@example.com', 'password123');
expect(result.success).to.be.false;
expect(result.message).to.equal('Email already in use');
});
it('should hash the password and register the user if the email is not in use', async function() {
// Szimuláljuk, hogy az email nem létezik
userRepositoryStub.resolves(null);
// Mockoljuk a bcrypt hash funkciót
const bcryptStub = sinon.stub(bcrypt, 'hash').resolves('hashedPassword');
const result = await userService.registerUser('newuser@example.com', 'plainPassword');
// Ellenőrizzük, hogy a bcrypt hash funkciót hívták
expect(bcryptStub.calledOnce).to.be.true;
expect(bcryptStub.calledWith('plainPassword')).to.be.true;
expect(result.success).to.be.true;
expect(result.message).to.equal('User registered successfully');
});
it('should not call bcrypt hash if the email already exists', async function() {
// Szimuláljuk, hogy az email már létezik
userRepositoryStub.resolves({ email: 'existing@example.com' });
const bcryptStub = sinon.stub(bcrypt, 'hash');
const result = await userService.registerUser('existing@example.com', 'plainPassword');
// Ellenőrizzük, hogy a bcrypt hash nem lett meghívva
expect(bcryptStub.called).to.be.false;
expect(result.success).to.be.false;
expect(result.message).to.equal('Email already in use');
});
});
A teszt futtatás természetesen hibát ad.
Hozzuk létre az ''services/userService.js'' implementációt az alábbiak szerint:
import bcrypt from 'bcrypt';
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async registerUser(email, password) {
const existingUser = await this.userRepository.findUserByEmail(email);
if (existingUser) {
return { success: false, message: 'Email already in use' };
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = { email, password: hashedPassword };
await this.userRepository.saveUser(newUser);
return { success: true, message: 'User registered successfully' };
}
}
export default UserService; // CommonJS helyett exportáljuk ESM szintaxissal