'Fixing mutations done on python's @staticmethod declaration

I have this class static method in python 3, with necessary test code

example.py:

class ExampleClass{
...
   @staticmethod
   def get_new_id: str, id: str):
      return {
          "new_id": "{}_{}_{}".format(
              datetime.utcnow().strftime("%Y%m%d%H%M%S"),
              id,
              name
          ),
          "old_id": id
      }
...
}  

test_example.py:

...
class TestExampleId(TestCase):
    @patch("example_folder.example.datetime", Mock(utcnow=Mock(return_value=datetime(1992, 1, 26, 12, 0, 0))))
    def given_id_name_example_object_returned(self):
        self.assertEqual(
            {
                'old_id': 'xxx',
                'new_id': '19920916120000_xxx_test_name'
            }, ExampleClass.get_new_id("xxx", "test_name")
        )

This is about it in terms of what mutations can do. But in mutmut, there is one surviving mutation, and it is the removal of @staticmethod. So i have two questions:

  • How can the test pass if the method called is from an uninstantiated class without the @staticmethod declaration?
  • what additional test case is needed to prevent a mutation surviving without the @staticmethod?

Thanks.



Solution 1:[1]

Came across the same thing, did some digging, here's what I found...

The tests pass because, in Python 3, calling a method on the class itself (rather than an instance of the class) runs the method as if it were static. I've not been able to find an 'official python' bit of documentation to cite for that, but can cite the accepted answer to this StackOverflow question. If anyone finds this actually documented anywhere, then edits are welcome!

Given that that's just how Python 3 works, it got me thinking about why that @staticmethod decorator is even needed. Since Python 3 will call the method statically when called on a class anyway, the decorator is only serving a purpose when we call it on an instance of the class. So you could probably do something like:

class TestExampleId(TestCase):
    @patch("example_folder.example.datetime", Mock(utcnow=Mock(return_value=datetime(1992, 1, 26, 12, 0, 0))))
    def given_id_name_example_object_returned(self):
        instance = ExampleClass()
        self.assertEqual(
            {
                'old_id': 'xxx',
                'new_id': '19920916120000_xxx_test_name'
            }, instance.get_new_id("xxx", "test_name")
        )

... and then the mutation will fail because the method inputs aren't right. In my case, my static methods are actually factory methods for the class, and it's nonsense to call my factory methods on an instance of the class. I decided to throw a particular error if the user tried to call a factory method on an instance of the class, and tested that that error was being thrown, that then covered the @staticmethod decorator. Once again, thoroughly testing has got me really thinking about the structure of my code!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Tim Hirst