slogan3

Ontwikkel een strategie voor het testen van legacy code met unit tests

description

Met deze prompt krijg je een op maat gemaakte aanpak voor het testen van legacy code, waardoor je de kwaliteit en onderhoudbaarheid van moeilijke codebases verbetert zonder de bestaande werking te breken. Het helpt je risico's te beheersen en stapsgewijs vertrouwen in de code op te bouwen.

prompt

Help me een strategie te ontwikkelen om legacy code effectief te voorzien van unit tests. Beschrijf hoe ik zonder de bestaande functionaliteit te breken betrouwbare unit tests kan schrijven voor verouderde of slec ...

try_prompt

generate_helper
disclaimerOnPageApi

ai_answers

provider: openai
model: gpt-4.1-nano
Het ontwikkelen van een strategie om legacy code te voorzien van betrouwbare unit tests zonder de bestaande functionaliteit te breken is een belangrijke stap. Hieronder geef ik een overzicht van technieken en stappen die je kunt volgen, inclusief voorbeelden: 1. Karakteriseringstests gebruiken - Doel: De bestaande functionaliteit vastleggen door te testen wat de code nu doet, niet wat het zou moeten doen. - Hoe: Voer de code uit met verschillende inputs en noteer de outputs en bijbehorende gedragingen. - Voorbeeld: ```python legacy = LegacyProcessor() input_data = "bestaande input" output = legacy.process(input_data) # Noteer de output # Maak een test die controleert dat dezelfde output wordt gegeven voor dezelfde input def test_karakterisering(): assert legacy.process(input_data) == output ``` 2. Afhankelijkheden isoleren - Doel: Externe afhankelijkheden (zoals databases, netwerkbronnen) isoleren zodat je de kernlogica kunt testen. - Techniek: Gebruik mocks of stubs voor afhankelijkheden. - Voorbeeld: ```python from unittest.mock import Mock legacy = LegacyProcessor() # Stel dat LegacyProcessor afhankelijk is van een database legacy.db = Mock() # Mock de methoden van de database legacy.db.get_data.return_value = "mocked data" ``` 3. Stapsgewijze refactoring - Doel: De code opdelen in kleinere, beter testbare stukken zonder functionaliteit te verliezen. - Hoe: - Identificeer complexe methoden en splits ze op. - Voeg eerst karakteriseringstests toe om de functionaliteit te vangen. - Refactor vervolgens voorzichtig, met steeds de tests die controleren of de functionaliteit behouden blijft. - Voorbeeld: ```python class LegacyProcessor: def process(self, data): # oorspronkelijke complexe logica result = self._stap1(data) result = self._stap2(result) return result def _stap1(self, data): # kleine, testbare eenheden pass def _stap2(self, data): # kleine, testbare eenheden pass ``` 4. Gebruik van 'Golden Master' testing - Vastleggen van de output voor een reeks inputs, zodat je zeker bent dat refactoring de output niet verandert. - Hoe: - Voer de code uit met bekende inputs en sla de outputs op. - Gebruik deze als referentie voor toekomstige tests. 5. Incrementele aanpak - Werk stap voor stap, voeg voor elke kleine verandering tests toe, en voer ze uit. - Zo minimaliseer je het risico op regressies. Samenvatting: - Begin met karakteriseringstests om de huidige functionaliteit vast te leggen. - Isolatie van afhankelijkheden maakt testen eenvoudiger. - Voer stapsgewijze refactoring uit, steeds met tests die de functionaliteit bewaken. - Gebruik mocks en stubs om externe afhankelijkheden te simuleren. - Werk incrementieel, voeg steeds meer unit tests toe en refactor geleidelijk. Door deze technieken toe te passen, kun je veilig en effectief oude, slecht gedocumenteerde code verbeteren en voorzien van betrouwbare tests.
provider: deepseek
model: deepseek-chat
Absoluut. Het voorzien van legacy code van unit tests is een uitdaging die veel ontwikkelaars tegenkomen. Het vereist een gedisciplineerde, voorzichtige aanpak. Hier is een strategie specifiek voor jouw `LegacyProcessor` voorbeeld, gebaseerd op de technieken die je noemde. ### Overkoepelende Strategie: Het "Scout, Isolate, Characterize, Refactor" Principe Het doel is niet onmiddellijk de code "mooi" te maken, maar eerst een **veiligheidsnet** van tests te bouwen. Pas daarna kun je met vertrouwen refactoren. --- ### Stap 1: Analyse en Scouten (Reconnaissance) **Doel:** Begrijp wat de code *nu daadwerkelijk* doet, niet wat de documentatie (als die er is) zegt of wat jij *denkt* dat hij doet. 1. **Lees de code:** Bestudeer de `process`-methode en alle methodes die hij aanroept. Identificeer: * **Externe Afhankelijkheden:** Roept het databases, API's, bestandssystemen, of andere klassen aan? (Bijv. `database.save()`, `requests.post()`, `logging.info()`). * **Zij-effecten:** Verandert het de staat van het systeem? (Bijv. wijzigt het globale variabelen, schrijft het naar een bestand). * **Conditionele Logica:** Zijn er veel `if/else`-vertakkingen of `try/except`-blokken? 2. **Manueel Testen:** Voer de code handmatig uit met verschillende soorten input (`data`). Gebruik debug-prints of een debugger om te zien welke paden worden bewandeld en wat de output is. Dit geeft je een eerste idee van het gedrag. --- ### Stap 2: Isolatie van Afhankelijkheden (Dependency Breaking) **Doel:** Maak de code testbaar door externe afhankelijkheden te vervangen door test-dubbels (mocks/stubs). Je wilt de *logica* van de `process`-methode testen, niet of de database online is. **Techniek: Dependency Injection (DI) / Monkey Patching** Stel je voor dat je `LegacyProcessor` er zo uitzag: ```python class LegacyProcessor: def process(self, data): # Complexe logica result = self._complex_calculation(data) # Externe afhankelijkheid - moeilijk te testen! database = DatabaseConnection() # Rechtstreeks geïnstantieerd database.save(result) # Nog een afhankelijkheid logger = Logger.get_logger() logger.info(f"Processed {data}") return result def _complex_calculation(self, data): # ... de eigenlijke logica pass ``` **Hoe isoleer je dit?** 1. **Refactor voor Injectie (Veiligste manier):** * Wijzig de `__init__` om afhankelijkheden te accepteren. ```python class LegacyProcessor: def __init__(self, database_connection, logger): self._db = database_connection self._logger = logger def process(self, data): result = self._complex_calculation(data) self._db.save(result) self._logger.info(f"Processed {data}") return result ``` * **Voordeel:** Helder en expliciet. * **Nadeel:** Vereist een (kleine) wijziging in de code, wat riskant is zonder tests. Doe dit heel voorzichtig. 2. **Monkey Patching (Snelste manier voor karakteriseringstests):** * Laat de code zoals hij is. In je testfile, "patch" je de externe afhankelijkheden voordat je de `LegacyProcessor` instantieert. Dit vervangt de echte classes/methods tijdelijk door mocks. ```python # test_legacy_processor.py from unittest.mock import Mock, patch from my_module import LegacyProcessor def test_process_saves_to_database(): # ARRANGE - Maak mocks mock_db = Mock() mock_logger = Mock() # PATCH de afhankelijkheden op de plek waar ze worden gebruikt with patch('my_module.DatabaseConnection', return_value=mock_db), \ patch('my_module.Logger.get_logger', return_value=mock_logger): processor = LegacyProcessor() test_data = {"key": "value"} # ACT result = processor.process(test_data) # ASSERT - Karakteriseer het gedrag! # Beweer dat save werd aangeroepen met het resultaat mock_db.save.assert_called_once_with(result) # Beweer dat de logger werd aangeroepen mock_logger.info.assert_called_once() ``` * **Voordeel:** Geen wijzigingen in productiecode nodig. Perfect voor de eerste karakteriseringstests. * **Nadeel:** Tests zijn iets fragieler omdat ze afhankelijk zijn van de import-paden (`'my_module.DatabaseConnection'`). --- ### Stap 3: Schrijven van Karakteriseringstests (Characterization Tests) **Doel:** Vastleggen en formaliseren van het *huidige, waargenomen gedrag* van de code. Deze tests beschermen je tegen onverwachte breuken tijdens latere refactoring. **Hoe werkt het?** 1. Schrijf een test voor een specifieke input. 2. **Run de test. Hij zal falen** (omdat je de verwachte output nog niet weet). 3. **Let op de *daadwerkelijke* output** die door de code wordt geproduceerd. 4. **Pas de test aan** om die daadwerkelijke output te verwachten. 5. **De test slaagt nu.** Je hebt een stukje gedrag vastgelegd. **Voorbeeld:** ```python def test_process_with_simple_input(): with patch('my_module.DatabaseConnection', return_value=Mock()), \ patch('my_module.Logger.get_logger', return_value=Mock()): processor = LegacyProcessor() input_data = "test" # Kies eenvoudige input # Run en observeer de werkelijke output (bijv. "TXST") actual_output = processor.process(input_data) # Nu pas je de test aan om dit gedrag vast te leggen assert actual_output == "TXST" # De waarde die je observeerde ``` **Herhaal dit proces** voor verschillende soorten input: normale data, randgevallen (`None`, lege strings), en data die fouten zou kunnen veroorzaken. Elke test legt een nieuw stukje gedrag vast. --- ### Stap 4: Stapsgewijze Refactoring (Boy Scout Rule) **Doel:** Nu je een veiligheidsnet van karakteriseringstests hebt, kun je de code veilig verbeteren. **Houd het simpel en incrementeel:** 1. **Renamen:** Verander slechte namen van variabelen, methodes of parameters in betere. IDE's doen dit veilig. 2. **Extract Method:** Isoleer een brok complexe logica in een nieuwe, goed genoemde methode. Schrijf *vervolgens* direct unit tests voor die nieuwe methode. Dit maakt de hoofd-`process`-methode eenvoudiger. 3. **Remove Duplication:** Zoek en elimineer duplicate code. 4. **Introduce Parameters:** Verander hardgecodeerde waarden in parameters (maak de code flexibeler). **Na elke kleine refactoring-stap:** **RUN ALL YOUR TESTS.** Als een test faalt, weet je precies welke wijziging het probleem veroorzaakte en kun je het direct herstellen. ### Samenvattend Stappenplan voor jouw `LegacyProcessor`: 1. **Scout:** Bestudeer de `process`-methode en identificeer alle afhankelijkheden (DB, log, andere services). 2. **Isoleer:** Gebruik `unittest.mock.patch` om deze afhankelijkheden in je tests te mocken. Dit is je eerste doorbraak naar testbaarheid. 3. **Karakteriseer:** Schrijf tests voor verschillende inputs, observeer de echte output, en leg deze vast in je test asserts. Bouw zo je veiligheidsnet. 4. **Refactor:** Begin met kleine, veilige wijzigingen (rename, extract method). Run na elke verandering je tests. 5. **Herhaal:** Blijf dit proces herhalen tot de code beter gestructureerd is en een dekkende testsuite heeft. Door deze methodische aanpak te volgen, verlaag je het risico aanzienlijk en transformeer je langzaam maar zeker onhandelbare legacy code in een getest, onderhoudbaar systeem. Succes!