1

I am testing a function that reads a file, does operations to the contents and returns a value depending on what it sees. The function testme I test lives in module.py. I am running python 2.7. I know I can accomplish this with

import unittest
import module
from mock import patch, mock_open

TestTestMe(unittest.TestCase):
    ...
    def test_test_me(self):
        with patch('module.open', mock_open(read_data='1 2')) as _:
          self.assertRaises(IndexError, module.testme, 'foo')
        with patch('module.open', mock_open(read_data='1 2 3')) as _:
          self.assertEquals(module.testme('foo'), 3)

etc.

However, I would like to (mostly to prevent repeated use of with statement, and also to be able to dynamically generate various read_data) be able to do this using the @patch as decorator defining my read_data with a function. Something like this. I will not repeat the class definition and imports.

def give_contents(x):
    if x == 'correct':
        return mock_open(read_data='1 2 3')
    else:
        return mock_open(read_data='a b c')

and then using the test function like:

@patch(module.open, side_effect=give_contents)
def test_test_me(self):
    self.assertEquals(module.testme('correct'), 3)

I keep running into TypeErrors such as

TypeError: test_testme() takes exactly 1 argument (2 given)

however I try to get around this. This is driving me crazy. Guidance would be greatly appreciated. If you want some additional details I may have omitted, please ask for specifics, and I will provide those.

Edit: Implementation of the function to be tested as requested. I'm sorry I omitted this as 'unimportant', it obviously should have been there.

def testme(filepath):
    with open(filepath, 'r') as f:
        line = f.readline().strip().split()
    return int(line[2])
user3274289
  • 2,426
  • 3
  • 16
  • 14
  • 1
    I'm not sure about that since you didn't include any occurence of a test_testme function in the code you gave us. However if it is the `module.testme` method you are using, you forgot to declare the string parameter in the method definition. Depending on your feedback, I might make this an answer. – Alceste_ Aug 05 '17 at 02:36
  • Consider not mocking at all here. You may be better off turning the "file" object into an argument and having other code you don't unit test open the file. Then you can pass in some kind of string IO object as a stub instead. Even if you can't get by with the string IO, you could still just pass in a mock as an argument instead. (I probably wouldn't assert on the mock even if I used one.) – jpmc26 Aug 05 '17 at 02:57
  • @Alceste_ I added the implementation. It would be great if you wrote a full answer. – user3274289 Aug 05 '17 at 15:23
  • I will, then, though it seems quite short for a full fledged answer. – Alceste_ Aug 05 '17 at 17:50

2 Answers2

2

I would consider the following:

from io import TextIOWrapper, BytesIO
from unittest.mock import patch, mock_open

def open_file(filename):
    with open(filename, 'r') as f:
        data = f.readline()
    return data

def string_byte_me(input):
    return TextIOWrapper(BytesIO(input))

def side_effect(filename, mode):
    if filename == 'correct':
        return string_byte_me(b'1 2 3')
    else:
        return string_byte_me(b'a b c')

m = mock_open()
m.side_effect = side_effect
@patch('builtins.open', m)
def test_open_file():
    assert open_file('correct') == '1 2 3'
    assert open_file('incorrect') == 'a b c'
test_open_file() # Passes.

This works by adding side_effect to the mock_open object after instantiation (not sure if there is a better way?). The returned side_effect must be capable of .readline() hence the TextIOWrapper.

Bordo
  • 33
  • 4
  • Used `StringIO(input)` in lieu of `TextIOWrapper(BytesIO(input))` but this was really helpful. – Adam Jul 11 '20 at 20:19
1

As I stated in a previous comment : I'm not sure about that since you didn't include any occurence of a test_testme function in the code you gave us. However if it is the module.testme method you are using, you forgot to declare the string parameter in the method definition. Depending on your feedback, I might make this an answer.

Edit : I wasn't exactly on the spot, as the argument your forgot was rather the self.

Apparently, this worked for you, so here is the promised answer.

Assuming the module.testme method you talked about is a function looking like :

TestTestMe(unittest.TestCase):
...
def testme(filepath):
with open(filepath, 'r') as f:
    line = f.readline().strip().split()
return int(line[2])

This function, however, is rather a method, as you are accessing it from an object. (doing module.testme('foo')), and as such, the first argument given to the call will always be the implicit self.

So what happens is, you have your function expecting one argument, a string ('foo'), but two are given even if self isn't explicit : '(self, 'foo')'.

Thus the error, which says you receive more arguments than you ask for. The correction is quite simple : add self to the expected arguments of testme.

Which would then become:

def testme(self, filepath):
    with open(filepath, 'r') as f:
        line = f.readline().strip().split()
    return int(line[2])

Hope this helped. Errors concerning amount of arguments are really often due to this kind of forgotten details. Although you don't need the self, it will always be passed as first positional argument (in python).

Have a nice day ! PS: Sorry for some strange english phrasing and repetitions.

Alceste_
  • 592
  • 4
  • 13