From ac1eb8533408e7c3586dfc68f86a8b9b195440c6 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Mon, 13 Oct 2025 23:23:25 +0200 Subject: [PATCH] refactor(backend): reduce duplication in tests (#10579) * refactor(backend): reduce duplication for tests * fix tests * fix text * adjust last test --- src/backend/InvenTree/InvenTree/unit_test.py | 59 +++++++++++- src/backend/InvenTree/build/test_api.py | 47 +++------- src/backend/InvenTree/company/test_api.py | 93 +++++-------------- src/backend/InvenTree/order/test_api.py | 95 +++++++------------- src/backend/InvenTree/part/test_api.py | 11 +-- src/backend/InvenTree/stock/test_api.py | 84 +++++------------ 6 files changed, 146 insertions(+), 243 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/unit_test.py b/src/backend/InvenTree/InvenTree/unit_test.py index 7479694132..352662b043 100644 --- a/src/backend/InvenTree/InvenTree/unit_test.py +++ b/src/backend/InvenTree/InvenTree/unit_test.py @@ -8,7 +8,7 @@ import re import time from contextlib import contextmanager from pathlib import Path -from typing import Optional +from typing import Callable, Optional, Union from unittest import mock from django.contrib.auth import get_user_model @@ -728,6 +728,63 @@ class InvenTreeAPITestCase( """Assert that dictionary 'a' is a subset of dictionary 'b'.""" self.assertEqual(b, b | a) + def run_output_test( + self, + url: str, + test_cases: list[Union[tuple[str, str], str]], + additional_params: Optional[dict] = None, + assert_subset: bool = False, + assert_fnc: Optional[Callable] = None, + ): + """Run a series of tests against the provided URL. + + Arguments: + url: The URL to test + test_cases: A list of tuples of the form (parameter_name, response_field_name) + additional_params: Additional request parameters to include in the request + assert_subset: If True, make the assertion against the first item in the response rather than the entire response + assert_fnc: If provided, call this function with the response data and make the assertion against the return value + """ + + def get_response(response): + if assert_subset: + return response.data[0] + if assert_fnc: + return assert_fnc(response) + return response.data + + for case in test_cases: + if isinstance(case, str): + param = case + field = case + else: + param, field = case + # Test with parameter set to 'true' + response = self.get( + url, + {param: 'true', **(additional_params or {})}, + expected_code=200, + msg=f'Testing {param}=true returns anything but 200', + ) + self.assertIn( + field, + get_response(response), + f"Field '{field}' should be present when {param}=true", + ) + + # Test with parameter set to 'false' + response = self.get( + url, + {param: 'false', **(additional_params or {})}, + expected_code=200, + msg=f'Testing {param}=false returns anything but 200', + ) + self.assertNotIn( + field, + get_response(response), + f"Field '{field}' should NOT be present when {param}=false", + ) + @override_settings( SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver'] diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index a941de65a2..b4c2a0cc98 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -1447,43 +1447,16 @@ class BuildLineTests(BuildAPITest): def test_output_options(self): """Test output options for the BuildLine endpoint.""" - url = reverse('api-build-line-detail', kwargs={'pk': 2}) - - # Test cases: (parameter_name, response_field_name) - test_cases = [ - ('bom_item_detail', 'bom_item_detail'), - ('assembly_detail', 'assembly_detail'), - ('part_detail', 'part_detail'), - ('build_detail', 'build_detail'), - ('allocations', 'allocations'), - ] - - for param, field in test_cases: - # Test with parameter set to 'true' - response = self.get( - url, - {param: 'true'}, - expected_code=200, - msg=f'Testing {param}=true returns anything but 200', - ) - self.assertIn( - field, - response.data, - f"Field '{field}' should be present when {param}=true", - ) - - # Test with parameter set to 'false' - response = self.get( - url, - {param: 'false'}, - expected_code=200, - msg=f'Testing {param}=false returns anything but 200', - ) - self.assertNotIn( - field, - response.data, - f"Field '{field}' should NOT be present when {param}=false", - ) + self.run_output_test( + reverse('api-build-line-detail', kwargs={'pk': 2}), + [ + 'bom_item_detail', + 'assembly_detail', + 'part_detail', + 'build_detail', + 'allocations', + ], + ) def test_filter_consumed(self): """Filter for the 'consumed' status.""" diff --git a/src/backend/InvenTree/company/test_api.py b/src/backend/InvenTree/company/test_api.py index 85219416c3..5e5ffd372f 100644 --- a/src/backend/InvenTree/company/test_api.py +++ b/src/backend/InvenTree/company/test_api.py @@ -538,31 +538,11 @@ class ManufacturerTest(InvenTreeAPITestCase): def test_output_options(self): """Test the output options for SupplierPart detail.""" - url = reverse('api-manufacturer-part-list') - - # Test cases: (parameter_name, response_field_name) - test_cases = [ - ('part_detail', 'part_detail'), - ('manufacturer_detail', 'manufacturer_detail'), - ('pretty', 'pretty_name'), - ] - - for param, field in test_cases: - # Test with parameter set to 'true' - response = self.get(url, {param: 'true', 'limit': 1}, expected_code=200) - self.assertIn( - field, - response.data['results'][0], - f"Field '{field}' should be present when {param}='true'", - ) - - # Test with parameter set to 'false' - response = self.get(url, {param: 'false', 'limit': 1}, expected_code=200) - self.assertNotIn( - field, - response.data['results'][0], - f"Field '{field}' should not be present when {param}='false'", - ) + self.run_output_test( + reverse('api-manufacturer-part-list'), + ['part_detail', 'manufacturer_detail', ('pretty', 'pretty_name')], + assert_subset=True, + ) class SupplierPartTest(InvenTreeAPITestCase): @@ -603,32 +583,15 @@ class SupplierPartTest(InvenTreeAPITestCase): def test_output_options(self): """Test the output options for SupplierPart detail.""" sp = SupplierPart.objects.all().first() - url = reverse('api-supplier-part-detail', kwargs={'pk': sp.pk}) - - # Test cases: (parameter_name, response_field_name) - test_cases = [ - ('part_detail', 'part_detail'), - ('supplier_detail', 'supplier_detail'), - ('manufacturer_detail', 'manufacturer_detail'), - ('pretty', 'pretty_name'), - ] - - for param, field in test_cases: - # Test with parameter set to 'true' - response = self.get(url, {param: 'true'}, expected_code=200) - self.assertIn( - field, - response.data, - f"Field '{field}' should be present when {param}='true'", - ) - - # Test with parameter set to 'false' - response = self.get(url, {param: 'false'}, expected_code=200) - self.assertNotIn( - field, - response.data, - f"Field '{field}' should not be present when {param}='false'", - ) + self.run_output_test( + reverse('api-supplier-part-detail', kwargs={'pk': sp.pk}), + [ + 'part_detail', + 'supplier_detail', + 'manufacturer_detail', + ('pretty', 'pretty_name'), + ], + ) def test_available(self): """Tests for updating the 'available' field.""" @@ -789,28 +752,12 @@ class SupplierPriceBreakAPITest(InvenTreeAPITestCase): def test_output_options(self): """Test the output options for SupplierPart price break list.""" - url = reverse('api-part-supplier-price-list') - test_cases = [ - ('part_detail', 'part_detail'), - ('supplier_detail', 'supplier_detail'), - ] - - for param, field in test_cases: - # Test with parameter set to 'true' - response = self.get(url, {param: 'true', 'limit': 1}, expected_code=200) - self.assertIn( - field, - response.data['results'][0], - f"Field '{field}' should be present when {param}='true'", - ) - - # Test with parameter set to 'false' - response = self.get(url, {param: 'false', 'limit': 1}, expected_code=200) - self.assertNotIn( - field, - response.data['results'][0], - f"Field '{field}' should not be present when {param}='false'", - ) + self.run_output_test( + reverse('api-part-supplier-price-list'), + ['part_detail', 'supplier_detail'], + additional_params={'limit': 1}, + assert_fnc=lambda x: x.data['results'][0], + ) def test_supplier_price_break_list(self): """Test the SupplierPriceBreak API list functionality.""" diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 1b559834f0..9442b09e78 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -315,12 +315,9 @@ class PurchaseOrderTest(OrderTest): def test_output_options(self): """Test the various output options for the PurchaseOrder detail endpoint.""" - url = reverse('api-po-detail', kwargs={'pk': 1}) - response = self.get(url, {'supplier_detail': 'true'}, expected_code=200) - self.assertIn('supplier_detail', response.data) - - response = self.get(url, {'supplier_detail': 'false'}, expected_code=200) - self.assertNotIn('supplier_detail', response.data) + self.run_output_test( + reverse('api-po-detail', kwargs={'pk': 1}), ['supplier_detail'] + ) def test_po_operations(self): """Test that we can create / edit and delete a PurchaseOrder via the API.""" @@ -863,17 +860,10 @@ class PurchaseOrderLineItemTest(OrderTest): def test_output_options(self): """Test PurchaseOrderLineItem output option endpoint.""" - url = reverse('api-po-line-detail', kwargs={'pk': 1}) - - response = self.get(url, {'part_detail': 'true'}, expected_code=200) - self.assertIn('part_detail', response.data) - response = self.get(url, {'part_detail': 'false'}, expected_code=200) - self.assertNotIn('part_detail', response.data) - - response = self.get(url, {'order_detail': 'true'}, expected_code=200) - self.assertIn('order_detail', response.data) - response = self.get(url, {'order_detail': 'false'}, expected_code=200) - self.assertNotIn('order_detail', response.data) + self.run_output_test( + reverse('api-po-line-detail', kwargs={'pk': 1}), + ['part_detail', 'order_detail'], + ) class PurchaseOrderDownloadTest(OrderTest): @@ -1774,11 +1764,9 @@ class SalesOrderTest(OrderTest): def test_output_options(self): """Test the output options for the SalesOrder detail endpoint.""" - url = reverse('api-so-detail', kwargs={'pk': 1}) - response = self.get(url, {'customer_detail': True}, expected_code=200) - self.assertIn('customer_detail', response.data) - response = self.get(url, {'customer_detail': False}, expected_code=200) - self.assertNotIn('customer_detail', response.data) + self.run_output_test( + reverse('api-so-detail', kwargs={'pk': 1}), ['customer_detail'] + ) class SalesOrderLineItemTest(OrderTest): @@ -1943,14 +1931,10 @@ class SalesOrderLineItemTest(OrderTest): def test_output_options(self): """Test the various output options for the SalesOrderLineItem detail endpoint.""" - url = reverse('api-so-line-detail', kwargs={'pk': 1}) - - options = ['part_detail', 'order_detail', 'customer_detail'] - for option in options: - response = self.get(url, {f'{option}': True}, expected_code=200) - self.assertIn(option, response.data) - response = self.get(url, {f'{option}': False}, expected_code=200) - self.assertNotIn(option, response.data) + self.run_output_test( + reverse('api-so-line-detail', kwargs={'pk': 1}), + ['part_detail', 'order_detail', 'customer_detail'], + ) class SalesOrderDownloadTest(OrderTest): @@ -2296,20 +2280,17 @@ class SalesOrderAllocateTest(OrderTest): def test_output_options(self): """Test the various output options for the SalesOrderAllocation detail endpoint.""" - url = reverse('api-so-allocation-list') - - options = [ - 'part_detail', - 'item_detail', - 'order_detail', - 'location_detail', - 'customer_detail', - ] - for option in options: - response = self.get(url, {f'{option}': True}, expected_code=200) - self.assertIn(option, response.data[0]) - response = self.get(url, {f'{option}': False}, expected_code=200) - self.assertNotIn(option, response.data[0]) + self.run_output_test( + reverse('api-so-allocation-list'), + [ + 'part_detail', + 'item_detail', + 'order_detail', + 'location_detail', + 'customer_detail', + ], + assert_subset=True, + ) class ReturnOrderTests(InvenTreeAPITestCase): @@ -2679,14 +2660,9 @@ class ReturnOrderTests(InvenTreeAPITestCase): def test_output_options(self): """Test the various output options for the ReturnOrder detail endpoint.""" - url = reverse('api-return-order-detail', kwargs={'pk': 1}) - - options = ['customer_detail'] - for option in options: - response = self.get(url, {f'{option}': True}, expected_code=200) - self.assertIn(option, response.data) - response = self.get(url, {f'{option}': False}, expected_code=200) - self.assertNotIn(option, response.data) + self.run_output_test( + reverse('api-return-order-detail', kwargs={'pk': 1}), ['customer_detail'] + ) class ReturnOrderLineItemTests(InvenTreeAPITestCase): @@ -2749,17 +2725,10 @@ class ReturnOrderLineItemTests(InvenTreeAPITestCase): def test_output_options(self): """Test output options for detail endpoint.""" - url = reverse('api-return-order-line-detail', kwargs={'pk': 1}) - options = ['part_detail', 'item_detail', 'order_detail'] - - for option in options: - # Test with option enabled - response = self.get(url, {option: True}, expected_code=200) - self.assertIn(option, response.data) - - # Test with option disabled - response = self.get(url, {option: False}, expected_code=200) - self.assertNotIn(option, response.data) + self.run_output_test( + reverse('api-return-order-line-detail', kwargs={'pk': 1}), + ['part_detail', 'item_detail', 'order_detail'], + ) def test_update(self): """Test updating ReturnOrderLineItem.""" diff --git a/src/backend/InvenTree/part/test_api.py b/src/backend/InvenTree/part/test_api.py index 079987fc77..b096962cd7 100644 --- a/src/backend/InvenTree/part/test_api.py +++ b/src/backend/InvenTree/part/test_api.py @@ -2666,13 +2666,10 @@ class BomItemTest(InvenTreeAPITestCase): def test_output_options(self): """Test that various output options work as expected.""" - url = reverse('api-bom-item-detail', kwargs={'pk': 3}) - options = ['can_build', 'part_detail', 'sub_part_detail'] - for option in options: - response = self.get(url, {f'{option}': True}, expected_code=200) - self.assertIn(option, response.data) - response = self.get(url, {f'{option}': False}, expected_code=200) - self.assertNotIn(option, response.data) + self.run_output_test( + reverse('api-bom-item-detail', kwargs={'pk': 3}), + ['can_build', 'part_detail', 'sub_part_detail'], + ) def test_add_bom_item(self): """Test that we can create a new BomItem via the API.""" diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 68bfbf1ca5..0d298cec6e 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -265,13 +265,9 @@ class StockLocationTest(StockAPITestCase): def test_output_options(self): """Test output options.""" - url = reverse('api-location-detail', kwargs={'pk': 1}) - - response = self.get(url, {'path_detail': 'true'}, expected_code=200) - self.assertIn('path', response.data) - - response = self.get(url, {'path_detail': 'false'}, expected_code=200) - self.assertNotIn('path', response.data) + self.run_output_test( + reverse('api-location-detail', kwargs={'pk': 1}), [('path_detail', 'path')] + ) def test_stock_location_structural(self): """Test the effectiveness of structural stock locations. @@ -1529,33 +1525,16 @@ class StockItemTest(StockAPITestCase): def test_output_options(self): """Test the output options for StockItemt detail.""" - url = reverse('api-stock-detail', kwargs={'pk': 1}) - - # Test cases: (parameter_name, response_field_name) - test_cases = [ - ('part_detail', 'part_detail'), - ('path_detail', 'location_path'), - ('supplier_part_detail', 'supplier_part_detail'), - ('location_detail', 'location_detail'), - ('tests', 'tests'), - ] - - for param, field in test_cases: - # Test with parameter set to 'true' - response = self.get(url, {param: 'true'}, expected_code=200) - self.assertIn( - field, - response.data, - f"Field '{field}' should be present when {param}='true'", - ) - - # Test with parameter set to 'false' - response = self.get(url, {param: 'false'}, expected_code=200) - self.assertNotIn( - field, - response.data, - f"Field '{field}' should not be present when {param}='false'", - ) + self.run_output_test( + reverse('api-stock-detail', kwargs={'pk': 1}), + [ + 'part_detail', + ('path_detail', 'location_path'), + 'supplier_part_detail', + 'location_detail', + 'tests', + ], + ) def test_install(self): """Test that stock item can be installed into another item, via the API.""" @@ -2223,19 +2202,10 @@ class StockTestResultTest(StockAPITestCase): def test_output_options(self): """Test output options for single item retrieval.""" - url = reverse('api-stock-test-result-detail', kwargs={'pk': 1}) - - response = self.get(url, {'user_detail': 'true'}, expected_code=200) - self.assertIn('user_detail', response.data) - - response = self.get(url, {'user_detail': 'false'}, expected_code=200) - self.assertNotIn('user_detail', response.data) - - response = self.get(url, {'template_detail': 'true'}, expected_code=200) - self.assertIn('template_detail', response.data) - - response = self.get(url, {'template_detail': 'false'}, expected_code=200) - self.assertNotIn('template_detail', response.data) + self.run_output_test( + reverse('api-stock-test-result-detail', kwargs={'pk': 1}), + ['user_detail', 'template_detail'], + ) class StockTrackingTest(StockAPITestCase): @@ -2334,23 +2304,13 @@ class StockTrackingTest(StockAPITestCase): def test_output_options(self): """Test output options.""" - url = self.get_url() - response = self.client.get( - url, {'item_detail': True, 'user_detail': True, 'limit': 2} + self.run_output_test( + self.get_url(), + ['item_detail', 'user_detail'], + additional_params={'limit': 2}, + assert_fnc=lambda x: x.data['results'][0], ) - for item in response.data['results']: - self.assertIn('item_detail', item) - self.assertIn('user_detail', item) - - response = self.client.get( - url, {'item_detail': False, 'user_detail': False, 'limit': 2} - ) - - for item in response.data['results']: - self.assertNotIn('item_detail', item) - self.assertNotIn('user_detail', item) - class StockAssignTest(StockAPITestCase): """Unit tests for the stock assignment API endpoint, where stock items are manually assigned to a customer."""