1

i am working in a django project and writing services to facilitate apis.in the services , i have class and class methods like this,

class ProductService(object):
    def delete_product(self, product_id, deleting_user):
        try:
            product = Product.objects.get(pk=product_id)
        except Product.DoesNotExist:
            raise ObjectDoesNotExist(_('no product found for this id {}'.format(product_id)))

        try:
            deleting_user = Customer.objects.get(owner=deleting_user)
        except Customer.DoesNotExist:
            raise ValidationError(_('No owner found for this deleting user'))

i have write following unit test for this method,

def setUp(self):
    self.product_service = ProductService()
    self.wrong_id = 0
    self.right_id = 1
    self.right_user = _user
    self.wrong_user = wrong_user  

def test_raise_does_not_exist_error_for_wrong_product_id(self):
    with self.assertRaises(ObjectDoesNotExist) as e:
        self.product_service.delete_product(
            self.product_id=self.wrong_id,
            user=self.right_user     
        )
    self.assertEqual(e.exception.message, 'no product found for this id {}'.format(wrong_id))

def test_raise_validtaion_error_for_wrong_deleting_user(self):
    with self.assertRaises(ObjectDoesNotExist) as e:
        self.product_service.delete_product(
            self.product_id=self.right_id,
            user=self.wrong_user     
        )
    self.assertEqual(e.exception.message, 'No owner found for this deleting user') 

so far so good, all the tests are OK!

but,say i have lots of test cases like this .that is, testing the 'errors' and if in future i have to change the error messages, then i have to change the test cases also, which could be a MESS, but on the other hand i also need to test the errors appropiately.

question is, how can i test the exceptions for different scenario?because the way i am testing, though it is ok for now,but for the future,it could be a mess,so i need some suggestions from you guys to handle this situation in an efficient way .

2
  • What's the question? Commented Aug 17, 2016 at 5:57
  • question is, how can i test the exceptions for different scenario? Commented Aug 17, 2016 at 5:59

3 Answers 3

6

We are using Enum for this case:

class ProductErrors(Enum):
    not_found = "Product {} not found"
    doesnt_exist = "Product {} doesn't exist"

This will allow you to use this enum in your tests and check it like that:

def test_raise_does_not_exist_error_for_wrong_product_id(self):
    with self.assertRaisesMessage(
        ObjectDoesNotExist, 
        ProductErrors.not_found.value.format(wrong_id)
    ):
        self.product_service.delete_product(
            self.product_id=self.wrong_id,
            user=self.right_user     
        )
Sign up to request clarification or add additional context in comments.

Comments

1

Now that I understand the original question that the goal is to allow changing the error text without having the tests break, I suggest to use translations. Instead of the full English text of the error, just use keys/codenames. I.e.: instead of _('No owner found for this deleting user')) use _('product_delete_error_no_owner_found') and translate the actual texts outside in the PO files.

Granted, this can lead to other problems (need to make sure that all your translation strings are actually translated, the dynamic content (variables) are not missed out during translation, and that all translation files are correctly deployed), but it would make the tests stable in the way the question desires it

Comments

-1

If I understand correctly, you are worried about the lot of repetitive code, which would make it hard to maintain the tests should anything change (e.g.: the method's signature accepting a new parameter, etc.).

Introducing a helper method might help (be careful not to create too many helper methods that use each other, as then the test code would become hard to follow), e.g.:

def test_raise_validtaion_error_for_wrong_deleting_user(self):
    self.assert_delete_product_raises_error(
        exception_cls=ObjectDoesNotExist,
        error_message='No owner found for this deleting user',
        product_id=self.right_id,
        user=self.wrong_user
    )

def assert_delete_product_raises_error(self, exception_cls, error_message, **delete_product_kwargs):
    with self.assertRaises(exception_cls) as raised:
         self.product_service.delete_product(**delete_product_kwargs)
    self.assertEqual(error_message, raised.exception.message)

This way should something change for the underlying method (e.g.: a new, required argument, that is not relevant for these tests, it can be added as a default in the helper method, and the actual tests would stay stable.

But as said above, avoid having too many helper methods and default values - if there are a lot of them, consider refactoring the test class into multiple test classes.

2 Comments

thanks for your answer,you answer is very usefull if there is any change in the signature in future, but what, if i change any error message explicitely from the service method,then again i have to change the error messages from the test cases.. exactly what i want to avoid.
It wasn't clear to me from the original question :( IMO in general, if one changes the production code, it is expected to change the tests covering them. But translations can help you there, let me add a different answer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.