Table of Contents

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

2. Szint

3. Szemléletmód

4. Nyelvezet

       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.

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.

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
</sxh>