0

Note: I'm used to using Dependency Injection with C# code, but from what I understand, dynamic languages like Ruby and Python are like play-doh not LEGOs, and thus don't need to follow use IoC containers, though there is some debate on if IoC patterns are still useful. In the code below I used fudge's .patch feature that provides the seams needed to mock/stub the code. However, the components of the code are thus coupled. I'm not sure I like this. This SO answer also explains that coupling in dynamic languages is looser than static ones, but does reference another answer in that question that says the tools for IoC are unneeded but the patterns are not. So a side question would be, "Should I have used DI for this?"

I'm using the following python frameworks:

  • Nose for unit testing
  • Fudge for fakes (stubs, mocking, etc)

Here is the resulting production code:

def to_fasta(seq_records, file_name):
    file_object = open(file_name, "w")
    Bio.SeqIO.write(seq_records, file_object, "fasta")
    file_object.close()

Now I did TDD this code, but I did it with the following test (which wasn't all the thorough):

@istest
@fudge.patch('__builtin__.open', 'Bio.SeqIO.write')
def to_fasta_writes_file(fake_open, fake_SeqIO):
    fake_open.is_a_stub()
    fake_SeqIO.expects_call()

    seq_records = build_expected_output_sequneces()
    file_path = "doesn't matter"

    to_fasta(seq_records, file_path)

Here is the updated test along with explicit comments to ensure I'm following the Four-Phase Test pattern:

@istest
@fudge.patch('__builtin__.open', 'Bio.SeqIO')
def to_fasta_writes_file(fake_open, fake_SeqIO):    
    # Setup
    seq_records = build_expected_output_sequneces()
    file_path = "doesn't matter"
    file_type = 'fasta'

    file_object = fudge.Fake('file').expects('close')

    (fake_open
        .expects_call()
        .with_args(file_path, 'w')
        .returns(file_object))

    (fake_SeqIO
         .is_callable()
         .expects("write")
         .with_args(seq_records, file_object, file_type))

    # Exercise
    to_fasta(seq_records, file_path)    

    # Verify (not needed due to '.patch')
    # Teardown

While the second example is more thorough, is this test overkill? Are there better ways to TDD python code? Basically, I'm looking for feedback on how I did with TDDing this operation, and would welcome any alternative ways to write either the test code or the production code.

Community
  • 1
  • 1
Matt
  • 14,353
  • 5
  • 53
  • 65
  • I don't see the advantage of testing that specific function. I think testing *everything* is just a waste of time. Simple functions like that one do not necessarily require a full test; often a simple doctest is enough. By the way: if you are not targetting python < 2.6 then using a `with` would allow you to avoid testing for calls to `close`, since they are guaranteed to happen. – Bakuriu Jul 13 '13 at 17:15
  • @Bakuriu, I've updated the note at the top, also, I'm new to python and this is why I'm asking these questions (I've come across reading about `doctest` after I wrote this, but this is the first I heard of `with`. As regards not testing "everything" how does one use TDD to achieve 100% code coverage? – Matt Jul 13 '13 at 17:25

1 Answers1

1

Think about what this function does and think about what you actually have responsibility for. It looks to me like: given some data and a file name, write the records in to the file in a particular format (fasta). You aren't actually responsible for the workings of Python file I/O, or how Bio.SeqIO works.

Your second version tests that:

  1. The file is opened for writing.
  2. That Bio.SeqIO.write is called with the expected parameters.
  3. The file is closed.

That looks pretty good. Most of this is simple, and some people may call it overkill, but the TDD approach can help remind you to do something like close the file (obvious, but we all forget stuff like that all the time). These tests also guard against such things as Bio.SeqIO.write being changed in the future to expect different parameters. You can either upgrade your version of the library and wonder why your program breaks, or upgrade your version of the library, run your tests, and know why and where it breaks.

Naturally you should write other tests for the case when you can't open the file, or any exceptions that Bio.SeqIO.write might throw.

Sean Redmond
  • 3,974
  • 22
  • 28
  • Thanks Sean, I also updated the note at the top around the time you posted this answer. This code isn't using DI, should it be? – Matt Jul 13 '13 at 17:29
  • I personally never find uses for DI but that could be because of the kinds of code I write. DI would do what here? Allow you to use something other that Bio.SeqIO with rewriting your code? Is that a likely problem? Would it take a lot of rewriting if you decided to make that change? – Sean Redmond Jul 13 '13 at 17:35
  • See [here](http://stackoverflow.com/a/2308557/452274) in addition to the SO answer linked in my Note in my question. I'm ambivalent right now with the need for DI or not in dynamic languages. Having one's program broken up into small independent components does sound appealing to me because I think that style of programming is easier to follow [SOLID](http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) principles. – Matt Jul 13 '13 at 17:50
  • I think it really depends on things that have a larger scope than this example and somewhat on personal style. I don't usually see a need for DI when writing Python, but that doesn't mean it doesn't have a role in solving other problems than mine. Don't we all bring some of our existing practices over when using a new or unfamiliar language? If you're comfortable with DI, use it. Maybe you'll find you use it less and less as you go. That's the other thing that thorough tests are good for -- being a safety net when you want to completely refactor the code! – Sean Redmond Jul 13 '13 at 18:24