2014-07-09 2 views
2

다른 프로젝트 (특히 subuser)에서 복사 한 다소 복잡한 (어쨌든) 기능 세트가 있습니다. 이 함수는 시스템에서 주어진 바이너리의 존재 여부 및 상태를 점검합니다. 그것들은 그대로 작동하지만 프로젝트에 적절한 테스트를하고 싶습니다. 나는 이것을 위해 python 3.4와 unittest.mock을 사용하고있다. 그래서 내 checks.py 모듈에는 다음과 같은 함수가 있습니다.모의를 사용하여 내 기능 테스트 전략

업데이트 : 최종 테스트 코드에서 함수 이름을 지정하는 데 몇 가지 스타일 항목이 변경되었습니다 (아래 참조).


import os 

def is_executable(fpath): 
    ''' 
    Returns true if the given filepath points to an executable file. 
    ''' 
    return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 


# Origonally taken from: http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python 
def query_path(test): 
    ''' 
    Search the PATH for an executable. 

    Given a function which takes an absolute filepath and returns True when the 
    filepath matches the query, return a list of full paths to matched files. 
    ''' 
    matches = [] 

    def append_if_matches(exeFile): 
     if is_executable(exeFile): 
      if test(exeFile): 
       matches.append(exeFile) 

    for path in os.environ['PATH'].split(os.pathsep): 
     path = path.strip('"') 
     if os.path.exists(path): 
      for fileInPath in os.listdir(path): 
       exeFile = os.path.join(path, fileInPath) 
       append_if_matches(exeFile) 

    return matches 


def which(program): 
    ''' 
    Check for existence and executable state of program. 
    ''' 
    fpath, fname = os.path.split(program) 

    if not fpath == '': 
     if is_executable(program): 
      return program 
    else: 
     def matches_program(path): 
      fpath, fname = os.path.split(path) 
      return program == fname 
    programMatches = query_path(matches_program) 
    if len(programMatches) > 0: 
     return programMatches[0] 

    return None 

이러한 것들은 훌륭하게 수행되며, PATH에 바이너리가 있는지 확인하고 실행 파일인지 확인한 후 첫 번째 결과를 반환합니다. 기본적으로 리눅스 'which'명령을 재생성합니다.

내 테스트 모듈은 지금까지 다음과 같습니다

참고 : 변명 다른 기능 이름 스타일, 최종 결과에 업데이트, 아래를 참조하십시오.


import unittest 
import unittest.mock as mock 

from myproject import checks 


class TestSystemChecks(unittest.TestCase): 

    def setUp(self): 
     pass 

    def tearDown(self): 
     pass 

    # This test works great 
    @mock.patch('os.path.isfile') 
    @mock.patch('os.access') 
    def test_isExecutable(self, mock_isfile, mock_access): 
     # case 1 
     mock_isfile.return_value = True 
     mock_access.return_value = True 

     self.assertTrue(
      checks.isExecutable('/some/executable/file')) 

     # case 2 
     mock_isfile.return_value = True 
     mock_access.return_value = False 

     self.assertFalse(
      checks.isExecutable('/some/non/executable/file')) 

    # THIS ONE IS A MESS. 
    @mock.patch('os.path.isfile') 
    @mock.patch('os.path.exists') 
    @mock.patch('os.access') 
    @mock.patch('os.listdir') 
    @mock.patch('os.environ') 
    def test_queryPATH(
      self, mock_isfile, mock_access, mock_environ, mock_exists, 
      mock_listdir): 
     # case 1 
     mock_isfile.return_value = True 
     mock_access.return_value = True 
     mock_exists.return_value = True 
     mock_listdir.return_value = [ 
      'somebin', 
      'another_bin', 
      'docker'] 
     mock_environ.dict['PATH'] = \ 
      '/wrong:' +\ 
      '/wrong/path/two:' +\ 
      '/docker/path/one:' +\ 
      '/other/docker/path' 
     target_paths = [ 
      '/docker/path/one/docker', 
      '/other/docker/path/docker'] 

     def isPathToDockerCommand(path): 
      return True 

     self.assertEqual(
      target_paths, 
      checks.queryPATH(isPathToDockerCommand)) 

    def test_which(self): 
     pass 

그래서 queryPATH()에 대한 테스트가 여기 내 질문입니다. 한 가지 기능을 너무 많이 사용하려고합니까? 매번 이러한 모든 모의 객체를 다시 만들거나 이러한 모든 테스트를 위해 setUp()에서 메타 객체 (또는 객체 세트)를 설정하는 방법이 있습니까? 아니면 원래 코드가 어떻게 작동하는지 이해하지 못하고 테스트를 올바로 설정하지 않았을 수도 있습니다. (모의 객체 사용은 정확합니다.) 이 테스트 수율을 실행 한 결과, 때문에 테스트의 복잡성과 함수 자체의

checks.queryPATH(isPathToDockerCommand)) 
AssertionError: Lists differ: ['/docker/path/one/docker', '/other/docker/path/docker'] != [] 

First list contains 2 additional elements. 
First extra element 0: 
/docker/path/one/docker 
- ['/docker/path/one/docker', '/other/docker/path/docker'] 
+ [] 

, 나는 내 테스트 권리를 디자인 할 수없는 이유를 모르겠어요. 이것은 내 단위 테스트에서 광범위하게 모의 작업을 사용하는 첫 번째 작업이며, 프로젝트 진행을 시작하기 전에 바로 가져 와서 TDD 스타일을 코딩 할 수 있습니다. 감사!

UPDATE : 여기

해결하고자 내 최종 결과는 모두이 세 가지 기능 영광에서처럼 보이는 결국 것입니다. @robjohncox 점에


import unittest 
import unittest.mock as mock 

from myproject import checks 

class TestSystemChecks(unittest.TestCase): 

    def setUp(self): 
     pass 

    def tearDown(self): 
     pass 

    @mock.patch('os.access') 
    @mock.patch('os.path.isfile') 
    def test_is_executable(self, 
          mock_isfile, 
          mock_access): 

     # case 1 
     mock_isfile.return_value = True 
     mock_access.return_value = True 

     self.assertTrue(
      checks.is_executable('/some/executable/file')) 

     # case 2 
     mock_isfile.return_value = True 
     mock_access.return_value = False 

     self.assertFalse(
      checks.is_executable('/some/non/executable/file')) 

    @mock.patch('os.listdir') 
    @mock.patch('os.access') 
    @mock.patch('os.path.exists') 
    @mock.patch('os.path.isfile') 
    def test_query_path(self, 
         mock_isfile, 
         mock_exists, 
         mock_access, 
         mock_listdir): 
     # case 1 
     # assume file exists, and is in all paths supplied 
     mock_isfile.return_value = True 
     mock_access.return_value = True 
     mock_exists.return_value = True 
     mock_listdir.return_value = ['docker'] 

     fake_path = '/path/one:' +\ 
        '/path/two' 

     def is_path_to_docker_command(path): 
      return True 

     with mock.patch.dict('os.environ', {'PATH': fake_path}): 
      self.assertEqual(
       ['/path/one/docker', '/path/two/docker'], 
       checks.query_path(is_path_to_docker_command)) 

     # case 2 
     # assume file exists, but not in any paths 
     mock_isfile.return_value = True 
     mock_access.return_value = True 
     mock_exists.return_value = False 
     mock_listdir.return_value = ['docker'] 

     fake_path = '/path/one:' +\ 
        '/path/two' 

     def is_path_to_docker_command(path): 
      return True 

     with mock.patch.dict('os.environ', {'PATH': fake_path}): 
      self.assertEqual(
       [], 
       checks.query_path(is_path_to_docker_command)) 

     # case 3 
     # assume file does not exist 
     mock_isfile.return_value = False 
     mock_access.return_value = False 
     mock_exists.return_value = False 
     mock_listdir.return_value = [''] 

     fake_path = '/path/one:' +\ 
        '/path/two' 

     def is_path_to_docker_command(path): 
      return True 

     with mock.patch.dict('os.environ', {'PATH': fake_path}): 
      self.assertEqual(
       [], 
       checks.query_path(is_path_to_docker_command)) 

    @mock.patch('os.listdir') 
    @mock.patch('os.access') 
    @mock.patch('os.path.exists') 
    @mock.patch('os.path.isfile') 
    def test_which(self, 
        mock_isfile, 
        mock_exists, 
        mock_access, 
        mock_listdir): 

     # case 1 
     # file exists, only take first result 
     mock_isfile.return_value = True 
     mock_access.return_value = True 
     mock_exists.return_value = True 
     mock_listdir.return_value = ['docker'] 

     fake_path = '/path/one:' +\ 
        '/path/two' 

     with mock.patch.dict('os.environ', {'PATH': fake_path}): 
      self.assertEqual(
       '/path/one/docker', 
       checks.which('docker')) 

     # case 2 
     # file does not exist 
     mock_isfile.return_value = True 
     mock_access.return_value = True 
     mock_exists.return_value = False 
     mock_listdir.return_value = [''] 

     fake_path = '/path/one:' +\ 
        '/path/two' 

     with mock.patch.dict('os.environ', {'PATH': fake_path}): 
      self.assertEqual(
       None, 
       checks.which('docker')) 

댓글 : 그는 그의 대답에 명시된 바와 같이

  1. 주문 또는 장식 사항.
  2. 데코레이터를 사용하여 사전 patch.dict을 패치하는 것은 이상하게도 다른 데코레이터처럼 인수로 함수에 객체를 전달할 필요가 없습니다. 소스 또는 뭔가에서 dict를 수정해야합니다.
  3. 테스트를 위해 장식 자 대신 컨텍스트를 변경하는 방법을 사용하여 다른 경로를 사용하여 다른 사례를 쉽게 테스트 할 수있었습니다.

답변

1

"나는 하나의 기능으로 너무 많은 것을 시도하고있다"는 질문에 대답은 아니오라고 생각합니다. 여기서 하나의 유닛을 테스트하고 복잡성을 더 해소하는 것은 불가능 해 보입니다. 는 단순히 환경에 필요한 복잡한 환경 설정 결과 일뿐입니다. 실제로 나는 당신의 노력에 박수를 보냅니다. 대부분의 사람들은 그 기능을보고 "너무 어려워서 시험에 신경 쓰지 않겠습니다"라고 생각합니다.시험 방법 서명에 모의 인수

주문 :

실패하는 테스트를 일으키는 원인이 될 수 있습니다 무엇에 관해서

밖으로 뛰어 두 가지가있다 당신은 각 모형의 위치에주의해야합니다

@mock.patch('function.one') 
@mock.patch('function.two') 
def test_something(self, mock_function_two, mock_function_one): 
    <test code> 

내가 당신의 각 기능 당신이하지에서 볼 : 서명은, 모의 개체가있는 당신은 예를 들어, 장식에서 그들을 선언 할 위해 조롱하는 프레임 워크에 의해 전달됩니다 모의 매개 변수가있다. 첫 번째 예제에서는 test_isExecutable, 모의 반환 값은 모두 True이므로 올바른 순서로 입력하십시오.

환경 사전에 PATH 비웃음 :을 내가하지가 취한 접근 방식이 방식으로는 최대 난 당신이 os.environ['PATH']이 때 무엇을 기대 반환됩니다 생각하지 않는다 설정되고, os.environ을 조롱 위해 일할 생각 테스트중인 코드에서 호출되었습니다 (하지만 잘못되었을 수도 있음). 다행스럽게도 모의에서는 여기에 @mock.patch.dict 데코레이터로 덮어 줘야합니다.

fake_path = '/wrong:' +\ 
      '/wrong/path/two:' +\ 
      '/docker/path/one:' +\ 
      '/other/docker/path' 

@mock.patch.dict('os.environ', {'PATH': fake_path}) 
@mock.patch('os.listdir') 
@mock.patch('os.access') 
@mock.patch('os.path.exists') 
@mock.patch('os.path.isfile') 
def test_queryPATH(self, 
        mock_isfile, 
        mock_exists, 
        mock_access, 
        mock_listdir, 
        mock_environ): 

    mock_isfile.return_value = True 
    mock_access.return_value = True 
    mock_exists.return_value = True 
    mock_listdir.return_value = [ 
     'somebin', 
     'another_bin', 
     'docker'] 

    target_paths = [ 
     '/docker/path/one/docker', 
     '/other/docker/path/docker'] 

    def isPathToDockerCommand(path): 
     return True 

    self.assertEqual(
     target_paths, 
     checks.queryPATH(isPathToDockerCommand)) 

면책 조항 : 이것은 올바른 순서로 모의 인수를 넣어 함께, 같은 발생한다 실제로이 코드를 테스트하지 않았으므로 보장보다는 가이드 라인으로 더를하시기 바랍니다 최선의 해결책은 올바른 방향으로 당신을 도울 수 있기를 바랍니다.

+0

이것은 실제로 도움이됩니다. 저는 장식 자의 순서가 중요하다는 것을 깨닫지 못했습니다. 나는 지금 훨씬 더 가깝다. 그러나 나는 전에 반대 한 적이 하나의 새로운 문제가 있습니다. 'mock.patch.dict'를 데코레이터로 패치하려고 할 때, 필자 함수는 항상 충분한 위치 인수를 불평합니다 :'TypeError : test_queryPATH() missing 1 필요한 위치 인자 : 'mock_environ''. 튜토리얼에서이 예제를 보았을 때, 모든 사람들은 항상 dicts에 대해 "with"컨텍스트 관리자를 사용합니다. 데코레이터가 내 함수를 arg로 보내지 않는 이유는 무엇입니까? – brianclements

+0

'test_queryPATH'의 서명에서'mock_environ' 매개 변수를 제거해야한다고 생각합니다. 사전을 조롱 할 때 프레임 워크가 모의 객체를 테스트에 전달하지 않는 것처럼 보입니다. – robjohncox

+0

기록을 보면, 당신이 맞았다 고 생각합니다. 'patch.dict'를 데코레이터로 패치하는 것은 어떤 객체를 함수로 전달할 필요가 없습니다. 결국 실제로 "with"컨텍스트 관리자를 사용하여 결국 동일한 테스트에서 다른 경로로 여러 사례를 테스트 할 수 있지만 결국 도움이되었습니다. 나중에이 질문에 대한 참조를 위해 최종 코드를 업데이트 할 것입니다. – brianclements