Как написать функциональный тест для службы DBUS, написанной на Python?

(Заголовок был: "Как написать модульный тест для службы DBUS, написанной на Python?")

Я начал писать сервис DBUS с использованием dbus-python, но у меня возникли проблемы с написанием тестового примера для него.

Вот пример теста, который я пытаюсь создать. Обратите внимание, что я поместил цикл событий GLib в setUp(), вот где проблема решается:

import unittest

import gobject
import dbus
import dbus.service
import dbus.glib

class MyDBUSService(dbus.service.Object):
    def __init__(self):
        bus_name = dbus.service.BusName('test.helloservice', bus = dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, '/test/helloservice')

    @dbus.service.method('test.helloservice')
    def hello(self):
        return "Hello World!"


class BaseTestCase(unittest.TestCase):

    def setUp(self):
        myservice = MyDBUSService()
        loop = gobject.MainLoop()
        loop.run()
        # === Test blocks here ===

    def testHelloService(self):
        bus = dbus.SessionBus()
        helloservice = bus.get_object('test.helloservice', '/test/helloservice')
        hello = helloservice.get_dbus_method('hello', 'test.helloservice')
        assert hello() == "Hello World!"

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

Моя проблема в том, что реализация DBUS требует, чтобы вы запустили цикл обработки событий, чтобы он мог начать диспетчеризацию событий. Обычный подход заключается в использовании gobject.MainLoop(). Start() GLib (хотя я не женат на этом подходе, если у кого-то есть лучшее предложение). Если вы не запускаете цикл обработки событий, сервис все еще блокируется, и вы также не можете запросить его.

Если я запускаю свою службу в тесте, цикл обработки событий блокирует завершение теста. Я знаю, что служба работает, потому что я могу запросить службу извне, используя инструмент qdbus, но я не могу автоматизировать это в тесте, который ее запускает.

Я подумываю сделать какой-то процесс разветвления внутри теста, чтобы справиться с этим, но я надеялся, что у кого-то может быть более точное решение или, по крайней мере, хорошее начальное место для того, как я мог бы написать такой тест.

6 ответов

Решение

С некоторой помощью из сообщения Али А мне удалось решить мою проблему. Цикл блокирующих событий нужно было запустить в отдельном процессе, чтобы он мог прослушивать события, не блокируя тест.

Помните, что заголовок моего вопроса содержал неверную терминологию, я пытался написать функциональный тест, а не модульный тест. Я знал об этом различии, но не осознал свою ошибку позже.

Я исправил пример в своем вопросе. Он напоминает пример "test_pidavim.py", но использует импорт для "dbus.glib" для обработки зависимостей цикла glib вместо кодирования во всех вещах DBusGMainLoop:

import unittest

import os
import sys
import subprocess
import time

import dbus
import dbus.service
import dbus.glib
import gobject

class MyDBUSService(dbus.service.Object):

    def __init__(self):
        bus_name = dbus.service.BusName('test.helloservice', bus = dbus.SessionBus())
        dbus.service.Object.__init__(self, bus_name, '/test/helloservice')

    def listen(self):
        loop = gobject.MainLoop()
        loop.run()

    @dbus.service.method('test.helloservice')
    def hello(self):
        return "Hello World!"


class BaseTestCase(unittest.TestCase):

    def setUp(self):
        env = os.environ.copy()
        self.p = subprocess.Popen(['python', './dbus_practice.py', 'server'], env=env)
        # Wait for the service to become available
        time.sleep(1)
        assert self.p.stdout == None
        assert self.p.stderr == None

    def testHelloService(self):
        bus = dbus.SessionBus()
        helloservice = bus.get_object('test.helloservice', '/test/helloservice')
        hello = helloservice.get_dbus_method('hello', 'test.helloservice')
        assert hello() == "Hello World!"

    def tearDown(self):
        # terminate() not supported in Python 2.5
        #self.p.terminate()
        os.kill(self.p.pid, 15)

if __name__ == '__main__':

    arg = ""
    if len(sys.argv) > 1:
        arg = sys.argv[1]

    if arg == "server":
        myservice = MyDBUSService()
        myservice.listen()

    else:
        unittest.main()

Простое решение: не тестировать модуль через dbus.

Вместо этого напишите свои модульные тесты, чтобы вызывать ваши методы напрямую. Это более естественно согласуется с природой юнит-тестов.

Вам также могут потребоваться некоторые автоматизированные интеграционные тесты, которые проверяют работу через dbus, но они не должны быть настолько полными или изолированными. Вы можете настроить программу, которая запускает реальный экземпляр вашего сервера, в отдельном процессе.

Вам просто нужно убедиться, что вы правильно обрабатываете свой основной цикл.

def refresh_ui():
    while gtk.events_pending():
       gtk.main_iteration_do(False)

Это будет запускать основной цикл gtk до тех пор, пока он не завершит обработку всего, а не просто запустить его и заблокировать.

Полный пример этого на практике, модульное тестирование интерфейса dbus, можно найти здесь: http://pida.co.uk/trac/browser/pida/editors/vim/test_pidavim.py

Вы также можете запустить основной цикл в отдельном потоке очень просто внутри вашего метода setUp.

Что-то вроде этого:

import threading
class BaseTestCase(unittest.TestCase):
    def setUp(self):
        myservice = MyDBUSService()
        self.loop = gobject.MainLoop()
        threading.Thread(name='glib mainloop', target=self.loop.run)
    def tearDown(self):
        self.loop.quit()

Я мог бы быть немного вне моей лиги здесь, так как я не знаю Python и только немного понимаю, что такое этот волшебный "dbus", но если я правильно понимаю, это требует от вас создания довольно необычной среды тестирования с расширенными циклами выполнения настройка / разборка и так далее.

Ответом на вашу проблему является использование насмешек. Создайте абстрактный класс, который определяет ваш интерфейс, а затем создайте из него объект для использования в вашем реальном коде. В целях тестирования вы создаете фиктивный объект, взаимодействующий через тот же интерфейс, но обладающий поведением, которое вы определите для целей тестирования. Вы можете использовать этот подход для "моделирования" объекта dbus, проходящего через цикл обработки событий, выполнения некоторой работы и т. Д., А затем просто сосредоточиться на тестировании того, как ваш класс должен реагировать на результат "работы", выполненной этим объектом.

Проверьте библиотеку python-dbusmock.

Он скрывает за вашими глазами некрасивую логику подпроцесса, поэтому вам не нужно беспокоиться об этом в своих тестах.

Другие вопросы по тегам