88

Our development team uses a PEP8 linter which requires a maximum line length of 80 characters.

When I'm writing unit tests in python, I like to have descriptive method names to describe what each test does. However this often leads to me exceeding the character limit.

This is an example of a function name that is too long.

class ClientConnectionTest(unittest.TestCase):
    
    def test_that_client_event_listener_receives_connection_refused_error_without_server(self):
        self.given_server_is_offline()
        self.given_client_connection()
        self.when_client_connection_starts()
        self.then_client_receives_connection_refused_error()

My options

  1. You could just write shorter method names!

I know, but I don't want to lose the descriptiveness of the test names.

  1. You could write multi-line comments above each test instead of using long names!

This is a decent idea, but then I won't be able to see the test names when running the tests inside my IDE (PyCharm).

  1. Perhaps you can continue the lines with a backslash (a logical line continuation character).

Unfortunately this isn't an option in Python, as mentioned in Dan's answer.

  1. You could stop linting your tests.

This makes sense in some ways, but it's nice to encourage a well-formatted test suite.

  1. You could increase the line length limit.

Our team likes having the limit because it helps keep the code readable on narrow displays, so this isn't the best option.

  1. You could remove test from the start of your methods.

This is not an option. Python's test runner needs all test methods to start with test or it won't pick them up.

Some test runners let you specify a regular expression when searching for test functions, although I'd rather not do this because it's extra setup for everyone working on the project.

  1. You could separate EventListener into its own class and test it separately.

The Event Listener is in its own class (and is tested). It's just an interface that gets triggered by events happening within ClientConnection. This kind of suggestion seems to have good intent, but is misdirected and doesn't help answer the original question.

  1. You could use a BDD Framework like Behave. It's designed for expressive tests.

This is true, and I hope to use more of them in the future. Although I'd still like to know how to split function names across lines.

Ultimately

Is there a way in Python to split a long function declaration across multiple lines, as in the following example?

def test_that_client_event_listener_receives_
  connection_refused_error_without_server(self):
    self.given_server_is_offline()
    self.given_client_connection()
    self.when_client_connection_starts()
    self.then_client_receives_connection_refused_error()

Will I have to bite the bullet and shorten it myself?

byxor
  • 5,930
  • 4
  • 27
  • 44
  • 8
    Why not use a descriptive function docstring? Then you could print it with `func.__doc__` – jkr Dec 04 '16 at 04:17
  • @Jakub I like that approach, although I think I'll still have the problem of not being able to see those docstrings beside each green light from PyCharm's test runner. (I haven't tried it yet) – byxor Dec 04 '16 at 04:19
  • 1
    I think that the only solution that I can come up with would be to write and [register](https://docs.python.org/3/library/codecs.html#codecs.register) your own encoding that joins the lines for you. You'd need to register before you import the test and be sure to set the coding for the test file ... (Note, I've never done this -- I'm not even 100% sure that this suggestion would actually work ...) – mgilson Dec 04 '16 at 04:29
  • @mgilson That's a very interesting idea. I have absolutely no idea how I would even go about attempting that. Would there be much of an overhead in doing this for a large project? – byxor Dec 04 '16 at 04:31
  • 63
    Stop linting your unit tests. – John Kugelman Dec 04 '16 at 04:47
  • 1
    @JohnKugelman Another good suggestion, but it's always nice to enforce a well formatted test suite. At least I could make the Jenkins build pass using your suggestion. – byxor Dec 04 '16 at 04:49
  • 55
    Then turn off this rule. It's a minor insanity that you're trying so hard to work around this lint rule rather than just disabling it. – John Kugelman Dec 04 '16 at 04:53
  • Maybe you're right. But aside from passing the linter, it'd be nice to know if this is possible with python in general. There could be other use cases than mine. – byxor Dec 04 '16 at 04:54
  • 13
    Revisit PEP8 https://www.python.org/dev/peps/pep-0008/, Good reasons to ignore guidelines: `When applying the guideline would make the code less readable, even for someone who is used to reading code that follows this PEP.` In your case that would be using shorter function name. – Akavall Dec 04 '16 at 05:02
  • @Akavall is correct here. PEP 8 should not be wielded as a weapon. Tools named after it are generally more restrictive than the actual document was ever intended to be. Disable the check (and if your tool doesn't let you disable a check for one file, *stop using that tool*). – rosuav Dec 04 '16 at 07:36
  • 3
    `test_client_listener_refuses_connection_on_no_server` – BlueRaja - Danny Pflughoeft Dec 04 '16 at 07:48
  • 57
    There are two hard problems in computer science, cache invalidation, naming things and off-by-one errors. – Surt Dec 04 '16 at 15:41
  • If question was C++ there was a way to do that with macros.. but that's really horrible to do. – CoffeDeveloper Dec 04 '16 at 16:38
  • 1
    What about improving the debug system to include a descriptive message of the method being tested? – CoffeDeveloper Dec 04 '16 at 17:15
  • 3
    Those tests aren't Python. They aren an internal testing DSL embedded in Python. Python's coding style doesn't apply. The same reason really, why tests should be DAMP, not DRY (Descriptive And Meaningful Phrases). – Jörg W Mittag Dec 04 '16 at 19:19
  • 1
    Apart from the other suggestions, it looks like you might just be able to create a EventListenerTest class and put those tests in there. This way you keep the test descriptive but are capable of shortening the method name. This is obviously only a reasonable idea if you have at least a handful of tests for it. – Voo Dec 04 '16 at 20:52
  • @Voo That's a good idea. The EventListener is a separate class, but it's very coupled (and has high cohesion) with the ClientConnection class. These are probably closer to integration tests than unit tests. – byxor Dec 04 '16 at 21:20
  • The reason for the line-length limit is to ensure readability (by humans) on all sorts of display. Your function names aren't sooooo long that your team cannot easily read them on their usual display set-up, are they? Your example triggers horizontal scrolling here on SO, for example. – alexis Dec 05 '16 at 12:30
  • 1
    Typical XY problem :) @JohnKugelman this feels like _the_ answer, since this is not really a question about splitting identifiers to multiple lines; it is a question about using a static analyser on a test suite and your answer is basically "production code has different rule than test suites". – CompuChip Dec 05 '16 at 15:28
  • @alexis The function names are somewhat easy to read, even on split views (assuming word wrap is enabled). They'd be a tiny bit easier to read if we could wrap them manually. – byxor Dec 05 '16 at 18:03
  • 1
    Can you use different lint settings for the unit tests? – Barmar Dec 06 '16 at 19:44
  • @Bamar Yes, but we'd not like to do this. – byxor Dec 06 '16 at 20:28
  • 1
    `class ClientEventListenerTest: def when_no_server_exists_it_gets_connection_refused` Your original name is pretty lengthy and not exactly worth fighting this much for if you ask me. Aside from that, your approach makes me think `ClientConnectionTest` is a huge class comprising many tests that could be relocated into smaller more focused units, like `ClientEventListenerTest`. – CakePlusPlus Dec 08 '16 at 20:58
  • @Alex The event listener is an interface. The methods in it are triggered by things happening with ClientConnection. Testing the event listener on its own has already been done. – byxor Dec 08 '16 at 21:20

7 Answers7

84

No, this is not possible.

In most cases such a long name would be undesirable from the standpoint of readability and usability of the function, though your use case for test names seems pretty reasonable.

The lexical rules of Python do not allow a single token (in this case an identifier) to be split across multiple lines. The logical line continuation character (\ at the end of a line) can join multiple physical lines into a single logical line, but cannot join a single token across multiple lines.

Dan Lenski
  • 76,929
  • 13
  • 76
  • 124
  • 2
    That's a shame. I still feel like there could be a magic solution somehow though. --- I should mention that I've tried the backslash in my post, just in case anyone mentions it to me. – byxor Dec 04 '16 at 04:36
  • 7
    Your best best is to use your descriptive name as the msg kwarg arg in in a self.assert* method. If the test passes you won't see it. But if the test fails your descriptive string will be available on the test result object. – B Rad C Dec 04 '16 at 04:37
  • 11
    It is worth noting that there is **exactly one situation** where the use of the line continuation character is acceptable: long `with` statements: `with expr1 as x, \ expr2 as y ...`. In **all** other cases, please, just wrap the expression in parenthesis: `(a_very_long + expression)` works fine, and is much more readable and *robust* then `a_very_long \ + expression`... the latter breaks by just adding a single space after the backslash! – Bakuriu Dec 04 '16 at 11:05
  • It really does seem like you have a legit use case here, but I can also see the counterarguments against allowing long wrapped identifiers :-/. @bradc, great point! – Dan Lenski Dec 04 '16 at 21:04
  • 3
    @Bakuriu - Whoa! I didn't know you couldn't wrap a `with` statement in parens. – mattmc3 Dec 04 '16 at 23:12
  • 2
    @mattmc3 The reason is simple: it is not an expression. AFAIK it's *literally* the only case where using parenthesis for continuation on a newline is simply not an option. – Bakuriu Dec 05 '16 at 07:35
  • @BrandonIbbotson It really isn't a shame. If your name is that long, then you need to find a new name, even for a test. If you can't write it on the board, or say it out loud without tripping over yourself, it is far too long. Even if the method is a test, find a better name. – krillgar Dec 05 '16 at 12:22
  • 2
    @krillgar I partially disagree. That test represents a full feature of my code because I've followed TDD. It's nice that I can list each feature off in a somewhat short sentence. – byxor Dec 05 '16 at 17:59
  • @BrandonIbbotson As a firm follower of TDD as well, I feel your name is too verbose. The fact that you're unable to see it on a screen (or getting lint warnings to the same effect) means several "standards" feel the same. For example, I would imagine that most of your tests begin with `test_that`. This would create a bunch of noise you're mentally ignoring as you go down the list of tests. I agree that the test name should be a short sentence, but in the scope of a test name, you're using run-on sentences. – krillgar Dec 05 '16 at 18:20
  • 2
    @krillgar On a side note, I've found that when I change `test_that` into `test`, my brain takes a little bit longer to understand the method name. I'm not sure why this phenomenon occurs, but I think it's because my brain associates `test_that` with the verb, test, rather than leaving the ambiguity of `test` being a noun _or_ a verb. – byxor Dec 05 '16 at 18:33
54

You could also write a decorator that mutates .__name__ for the method.

def test_name(name):
    def wrapper(f):
        f.__name__ = name
        return f
    return wrapper

Then you could write:

class ClientConnectionTest(unittest.TestCase):
    @test_name("test_that_client_event_listener_"
    "receives_connection_refused_error_without_server")
    def test_client_offline_behavior(self):
        self.given_server_is_offline()
        self.given_client_connection()
        self.when_client_connection_starts()
        self.then_client_receives_connection_refused_error()

relying on the fact that Python concatenates source-adjacent string literals.

Sean Vieira
  • 155,703
  • 32
  • 311
  • 293
  • 3
    This is a _very_ good idea. It looks very readable too. I'll try it now and see if my IDE shows the longer function names. – byxor Dec 04 '16 at 19:56
  • 2
    Unfortunately the decorator doesn't get applied before the test runs in PyCharm, meaning I can't see the descriptive names from my test runner. – byxor Dec 04 '16 at 19:59
  • 2
    I think you'll want to decorate `wrapper` with `@functools.wraps(f)`. –  Dec 05 '16 at 00:08
  • 1
    In this case `f` is being mutated and returned, @Mego - `@functools.wraps` is for when you're returning a replacement function that you want to look like the function you're replacing. – Sean Vieira Dec 05 '16 at 00:12
  • 1
    @Brandon - that's too bad. Sounds like a candidate for a bug report for PyCharm (assuming that other reporters pick up on the name change.) – Sean Vieira Dec 05 '16 at 00:13
  • 2
    This is the best have-your-cake-and-eat-it-too solution; it combines all of the features that @BrandonIbbotson was looking for. Too bad PyCharm doesn't quite grok it yet. – Dan Lenski Dec 06 '16 at 00:08
  • 3
    Even better, modify the decorator to generate a descriptive name from the function's docstring. – Nick Sweeting Dec 07 '16 at 01:22
36

Per the answer to this question:How to disable a pep8 error in a specific file?, use the # nopep8 or # noqa trailing comment to disable PEP-8 for a long line. It's important to know when to break the rules. Of course, the Zen of Python would tell you that "Special cases aren't special enough to break the rules."

Community
  • 1
  • 1
mattmc3
  • 17,595
  • 7
  • 83
  • 103
  • 5
    That's actually a fantastic idea because it lets me lint the rest of the test files. I just tested it and it works. I also get to keep all the benefits of the long method names. --- My only concern is that the team won't like seeing the `# nopep8` comment littered throughout the tests ;) – byxor Dec 04 '16 at 05:05
9

We can applying decorator to the class instead of method since unittest get methods name from dir(class).

The decorator decorate_method will go through class methods and rename method's name based on func_mapping dictionary.

Thought of this after seeing decorator answer from @Sean Vieira , +1 from me

import unittest, inspect

# dictionary map short to long function names
func_mapping = {}
func_mapping['test_client'] = ("test_that_client_event_listener_receives_"
                               "connection_refused_error_without_server")     
# continue added more funtion name mapping to the dict

def decorate_method(func_map, prefix='test_'):
    def decorate_class(cls):
        for (name, m) in inspect.getmembers(cls, inspect.ismethod):
            if name in func_map and name.startswith(prefix):
                setattr(cls, func_map.get(name), m) # set func name with new name from mapping dict
                delattr(cls, name) # delete the original short name class attribute
        return cls
    return decorate_class

@decorate_method(func_mapping)
class ClientConnectionTest(unittest.TestCase):     
    def test_client(self):
        # dummy print for testing
        print('i am test_client')
        # self.given_server_is_offline()
        # self.given_client_connection()
        # self.when_client_connection_starts()
        # self.then_client_receives_connection_refused_error()

test run with unittest as below did show the full long descriptive function name, thinks it might works for your case though it may not sounds so elegant and readable from the implementation

>>> unittest.main(verbosity=2)
test_that_client_event_listener_receives_connection_refused_error_without_server (__main__.ClientConnectionTest) ... i am client_test
ok
Skycc
  • 3,496
  • 1
  • 12
  • 18
8

Sort of a context-specific approach to the problem. The test case you've presented actually looks very much like a Natural Language format of describing the necessary steps for a test case to take.

See if using the behave Behavior Driver development style framework would make more sense here. Your "feature" might look like (see how the given, when, then reflect what you had):

Feature: Connect error testing

  Scenario: Client event listener receives connection refused error without server
     Given server is offline
      when client connect starts
      then client receives connection refused error

There is also relevant pyspecs package, sample usage from a recent answer on a related topic:

Community
  • 1
  • 1
alecxe
  • 462,703
  • 120
  • 1,088
  • 1,195
  • I was thinking of mentioning that I knew there were BDD options like `behave`. However I didn't want to distract people too much in my question. It looks like a really nice framework and I'll probably use it in the future. I actually asked my team if I could use it in this project, but they didn't want to tests to look "weird" ;) --- I've never seen pyspecs before. Thanks for the suggestion. – byxor Dec 04 '16 at 05:38
  • 1
    @BrandonIbbotson gotcha, I understand why you did not want to mention it - makes perfect sense. `pyspecs`, by the way, might be easier to integrate into your test codebase though - a more "python" way of doing BDD - no these feature files needed. Thanks! – alecxe Dec 04 '16 at 05:40
6

The need for this kind of names may hint at other smells.

class ClientConnectionTest(unittest.TestCase):
   def test_that_client_event_listener_receives_connection_refused_error_without_server(self):
       ...

ClientConnectionTest sounds pretty broad (and not at all like a testable unit), and is likely a large class with plenty of tests inside that could be refocused. Like this:

class ClientEventListenerTest(unittest.TestCase):
  def receives_connection_refused_without_server(self):
      ...

"Test" is not useful in the name because it's implied.

With all the code you've given me, my final advice is: refactor your test code, then revisit your problem (if it's still there).

CakePlusPlus
  • 943
  • 7
  • 15
  • The event listener is an interface. The methods in it are triggered by things happening with ClientConnection. Testing the event listener on its own has already been done. Personally I think ClientConnection follows SRP pretty well, but I could be biased (and you can't see it). --- Python test names _must_ begin with `test` or the test runner doesn't pick them up. – byxor Dec 08 '16 at 21:23
  • 1
    @BrandonIbbotson Ah, I get it now, you're testing that the *client connection* triggers something in the event listener. That would've been more obvious with a name like "test_that_connection_without_server_triggers_connection_refused_event". The "test" part requirement is awful because it forces you to either go with awkward names or names full of useless glue. – CakePlusPlus Dec 08 '16 at 21:32
  • That's a better method name. I might rename a couple of those methods how you've suggested. Although I'll probably still have a lot of methods over 80 chars – byxor Dec 08 '16 at 21:35
  • From what I see you can nest classes in Python. Does the test runner handle that? Maybe you can split the insides of ClientConnectionTest into topics that are nested classes containing related tests. That way the topic's class carries the part of the name that you don't need to write on each test. – CakePlusPlus Dec 08 '16 at 21:41
  • I probably could, but I think that would be ugly and keep it even harder to stay within the character limit. I could always make a separate`ClientEventListenerTest` class – byxor Dec 08 '16 at 21:44
  • 1
    Yeah figured that might be the case. Not sure what else to suggest then. Maybe give extending the character limit a go anyway, we did that ourselves and in the end realized it's not that big of a deal and everyone had room to welcome more than 80 char lines. Good luck! – CakePlusPlus Dec 08 '16 at 21:47
5

The shorter function name solution has a lot of merit. Think about what is really needed in your actual function name and what is supplied already.

test_that_client_event_listener_receives_connection_refused_error_without_server(self):

Surely you already know it's a test when you run it? Do you really need to use underscores? are words like 'that' really required for the name to be understood? would camel case be just as readable? how about the first example below as a rewriting of the above (character count = 79): Accepting a convention to use abbreviations for a small collection of common words is even more effective, e.g. Connection = Conn, Error = Err. When using abbreviations you have to be mindful of the context and only use them when there is no possiblity of confusion - Second example below. If you accept that there's no actual need to mention the client as the test subject in the method name as that information is in the class name then the third example may be appropriate. (54) characters.

ClientEventListenerReceivesConnectionRefusedErrorWithoutServer(self):

ClientEventListenerReceivesConnRefusedErrWithoutServer(self):

EventListenerReceiveConnRefusedErrWithoutServer(self):

I'd also agree with the the suggestion from B Rad C "use descriptive name as the msg kwarg arg in in a self.assert" You should only be interested in seeing output from failed tests when the testsuite is run. Verification that you have all the necessary tests written shouldn't depend on having the method names so detailed.

P.S. I'd probably also remove 'WithoutServer' as superfluous as well. Shouldn't the client event handler receive the event in the case that the server isn't contactable for any reason? (although tbh I'd think that it would be better that if they client can't connect to a server it receives some sort of 'connection unavailable' , connection refused suggests that the server can be found but refuses the connection itself.)

Charemer
  • 196
  • 1
  • 9
  • 1
    TL;DR - please compare the length of your answer with other answer. – MarianD Dec 04 '16 at 13:45
  • 3
    MarianD : So sorry, but the answer was given for the OP who may be bothered to take a minute to read it and addressed several strategies for shortening the name with constructive examples and rationale. If you want the short version ... "Avoid unnecessary words and punctuation and shorten common words consistently" - is that brief enough? – Charemer Dec 04 '16 at 15:39
  • 3
    With python's unittest library, each test method has to begin with `test` otherwise the test runner doesn't pick it up. – byxor Dec 04 '16 at 19:54
  • 1
    @BrandonIbbotson `test_EventListenerReceiveConnRefusedErrWithoutServer(self):` – Hendry Dec 08 '16 at 12:00
  • 1
    I like CamelCase, but I think it appears to b a violation of PEP 8: " Use the function naming rules: lowercase with words separated by underscores as necessary to improve readability. " – Scooter Dec 08 '16 at 21:49