Pythonic way do testowania np "subprocess"?

0

Mam klasę która korzysta z subprocess.call,i zastanawiam się jak ją właściwie przetestować.

Pisząc w Javie, na pewno użyłbym Dependency Injection i po prostu wsadził w teście mocka. Python to jednak nie java. W większości pytań nt testowania takich metod większość jednak jest za tym żeby w testach po prostu podmienić globalny subprocess.call na mocka.

Nie jestem pewien które z nich jest okej, ktoś coś doradzi?

0

Może mockowanie zaimportowanych modułów byłoby najlepsze?

1

Możesz użyć https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch
Albo niech funkcja będzie wstrzykiwana do klasy przez konstruktor. Domyślnie niech to będzie subprocess.call, a w testach możesz sobie podmienić na co chcesz.

1
import subprocess
import unittest.mock

class Foo():
    def foo(self):
        subprocess.call("pwd")
        return "ok"


class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('subprocess.call'):
            self.assertEqual(Foo().foo(), "ok")


if __name__ == '__main__':
    unittest.main()
0
Spearhead napisał(a):
import subprocess
import unittest.mock

class Foo():
    def foo(self):
        subprocess.call("pwd")
        return "ok"


class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('subprocess.call'):
            self.assertEqual(Foo().foo(), "ok")


if __name__ == '__main__':
    unittest.main()

A czemu nie działa jak zaimportuję call?

from subprocess import call
import unittest.mock

class Foo():
    def foo(self):
       call("pwd")
        return "ok"
0

Inne przestrzenie nazw.

from subprocess import call
import unittest.mock

class Foo():
    def foo(self):
        call("pwd")
        return "ok"


class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('__main__.call'):
            self.assertEqual(Foo().foo(), "ok")


if __name__ == '__main__':
    unittest.main()

Patrz: https://stackoverflow.com/questions/11351382/mock-patching-from-import-statement-in-python

0
Spearhead napisał(a):

Inne przestrzenie nazw.

from subprocess import call
import unittest.mock

class Foo():
    def foo(self):
        call("pwd")
        return "ok"


class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('__main__.call'):
            self.assertEqual(Foo().foo(), "ok")


if __name__ == '__main__':
    unittest.main()

Patrz: https://stackoverflow.com/questions/11351382/mock-patching-from-import-statement-in-python

Czyli, gdybym zrobił

import subprocess
import unittest.mock

from my_second_file import Foo

class TestStringMethods(unittest.TestCase):
    def test_foo(self):
            with unittest.mock.patch('__main__.call'):   # to nie zadziała, bo "call()" jest wołany w "my_second_file", tak?
            self.assertEqual(Foo().foo(), "ok")

if __name__ == '__main__':
    unittest.main()
0
Spearhead napisał(a):

Inne przestrzenie nazw.

No to nie, to ja chyba podziękuję za takie rozwiązanie bo testy wiedzą wtedy o szczegółach implementacyjnych klasy (np wiedzą czy używam subprocess.call() zamiast call()).

0
import subprocess
import unittest.mock

from my_second_file import Foo

class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('my_second_file.call'):
            self.assertEqual(Foo().foo(), "ok")

if __name__ == '__main__':
    unittest.main()
0
Spearhead napisał(a):
import subprocess
import unittest.mock

from my_second_file import Foo

class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('my_second_file.call'):
            self.assertEqual(Foo().foo(), "ok")

if __name__ == '__main__':
    unittest.main()

No właśnie tego się obawiałem :/ Nie mogę swobodnie zmienić implementacji bo testy zbyt dużo wiedzą o tej funkcji :/

0

No to nie, to ja chyba podziękuję za takie rozwiązanie bo testy wiedzą wtedy o szczegółach implementacyjnych klasy (np wiedzą czy używam subprocess.call() zamiast call()).

Globalnie zdefiniowane nazwy to nie szczegół implementacyjny klasy, a własność samego modułu, który chcesz mockować. call nie musi pochodzić z subprocess, może po prostu być globalną funkcją o takiej nazwie. Stąd zarówno moduł jak i test musi mieć wspólne konwencje co do nazewnictw i importów.

0
Spearhead napisał(a):

No to nie, to ja chyba podziękuję za takie rozwiązanie bo testy wiedzą wtedy o szczegółach implementacyjnych klasy (np wiedzą czy używam subprocess.call() zamiast call()).

Globalnie zdefiniowane nazwy to nie szczegół implementacyjny klasy, a własność samego modułu, który chcesz mockować. call nie musi pochodzić z subprocess, może po prostu być globalną funkcją o takiej nazwie. Stąd zarówno moduł jak i test musi mieć wspólne konwencje co do nazewnictw i importów.

Rozumiem powody dla których to tak działa, nie musisz mi tłumaczyć jak duży ma to sens.

Po prostu mówię że takie mi nie odpowiada, bo zmiany w kodzie (np zaimportowanie funkcji albo wyniesienie logiki do innego pliku) sprawiłoby że testy sfailują.

Ja chcę tak napisać te testy, że jak ktoś zamieni subprocess.call() na call() albo wyniesie logikę do klasy w innymi pliku, to testy dalej będą działać.

0

@TomRiddle: a co powiesz na:

from subprocess import call
import unittest.mock

class Foo():

	def __init__(self, func):
		self._func = func

	def foo(self):
		self._func('pwd')
		return "ok"

class TestStringMethods(unittest.TestCase):

	def setUp(self):
		# oczywiście ta funkcja może (a nawet powinna) być gdzieś indziej
		def mock_call(cmd):
			if cmd == 'pwd':
				return '/etc'
			else:
				raise NotImplementedError()  # whatever
		self.foo = Foo(func=mock_call)
    
	def test_foo(self):
		self.assertEqual(self.foo.foo(), "ok")

if __name__ == '__main__':
	unittest.main()

https://ideone.com/PqzZb9

0

Ja chcę tak napisać te testy, że jak ktoś zamieni subprocess.call() na call() albo wyniesie logikę do klasy w innymi pliku, to testy dalej będą działać.

Zmiana kodu powoduje, że część testów przestaje działać i wymaga aktualizacji. No niespodzianka xD

Tak czy inaczej, już pisałem, to jest Python, możesz podmienić wszystko, łącznie z samym Foo.foo

import unittest.mock
from my_second_file import Foo

class TestStringMethods(unittest.TestCase):
    def test_foo(self):
        with unittest.mock.patch('my_second_file.Foo.foo', return_value="ok"):
            self.assertEqual(Foo().foo(), "ok")

if __name__ == '__main__':
    unittest.main()

Teraz nie ma znaczenia czego używa Foo w swojej implementacji.

0

Zmiana kodu powoduje, że część testów przestaje działać i wymaga aktualizacji. No niespodzianka xD

No ja bym dokładnie tak chciał. Zmiana implementacji nie powinna powodować że testy failują.

1

Ja nie rozumiem co ty tu chcesz testować. Czy python działa? Napisz normalny test integracyjny, który sprawdza czy efekt się zgadza i tyle. A to czy ktoś tam woła os.system() czy subprocess.call czy robi jeszcze coś innego w ogóle nie powinno cię obchodzić. Twój problem polega na tym ze robisz jakiś whitebox test który sprawdza jak wygląda kod a nie co kod robi.

0
Shalom napisał(a):

Ja nie rozumiem co ty tu chcesz testować. Czy python działa? Napisz normalny test integracyjny, który sprawdza czy efekt się zgadza i tyle. A to czy ktoś tam woła os.system() czy subprocess.call czy robi jeszcze coś innego w ogóle nie powinno cię obchodzić.

No, tylko ja nie chcę odpalać prawdziwego procesu. Chciałbym wsadzić "udawany proces", któremu napisze jaki ma mieć exit-code i standard output. Ewentualnie jakiś "pseudo-proces", albo "tmp-proces".

Jak piszę testy pod klasę które operują na plikach, to tworzę folder /tmp i taką siężkę przekazuję w dół. Myślę jak zrobić coś podobnego z odpalaniem procesu, żeby nie polegać na plikach systemowych, tylko żeby testy były niezależne.

Shalom napisał(a):

Twój problem polega na tym ze robisz jakiś whitebox test który sprawdza jak wygląda kod a nie co kod robi.

No przecież nawet jedną odpowiedź wyżej napisałem że nie chcę wiedzieć jak to działa, żebym mógł sobie zmieniać implementację, tak żeby testy przechodził.

TomRiddle napisał(a):

Zmiana kodu powoduje, że część testów przestaje działać i wymaga aktualizacji. No niespodzianka xD

No ja bym dokładnie tak chciał. Zmiana implementacji nie powinna powodować że testy failują.

1 użytkowników online, w tym zalogowanych: 0, gości: 1