Repository: opsdroid/opsdroid (language: python)

Issue:
Migrate tests from unittest to pytest
Our existing test suite has been written with the Python [`unittest`](https://docs.python.org/3/library/unittest.html) framework. However, as the test suite has grown and opsdroid has become more complex we are running into issues with the tests. Mainly around setting up and tearing down tests.

The @opsdroid/maintainers team have decided that we want to migrate all tests to be written with the [`pytest`](https://docs.pytest.org/en/latest/contents.html) framework instead so that we can make better use of fixtures. Fixtures are more reusable and portable and should help reduce complexity all over.

There's a lot to be done but it can be done piece by piece as `pytest` can run tests in either format. So if you wish to help in the effort you can start by searching the codebase for unittest suites. These are classes which are subclassed from `unittest.TestCase` or `asynctest.TestCase`, so searching all files for `unittest.TestCase` and `asynctest.TestCase` should be a good place to start.

For detailed information on running the test suite and contributing to opsdroid [see the docs](https://docs.opsdroid.dev/en/latest/contributing/index.html). But the quickest way to get started us with [`tox`](https://tox.readthedocs.io/en/latest/).

```bash
pip install -U tox  # You only need to install tox once

tox -e py36,lint  # Run the Python 3.6 tests (the lowest version we support) and the linter
```

Once you have found a test suite you wish to convert there are a few steps you need to follow to convert from unittest to pytest:
- Move tests from top level `tests` directory to a nested `tests` directory in opsdroid. Create one in an appropriate place if there isn't already one.
- Remove test from class, pytest tests are just regular functions.
- Change assertions to use regular `assert` or [pytest assertions](https://docs.pytest.org/en/latest/assert.html).
- Mark async tests. In unittest we write async tests by using the `asynctest.TestCase`, but in pytest we decorate our tests with `@pytest.mark.asyncio` instead.
- Move setup operations to fixtures. If a test class contains a `setUp` method anything created here should become a fixture. Check the existing fixtures in `conftest.py` before creating new ones.
- Add docstrings to tests and fixtures to explain what they do. We have been pretty rubbish with this up until now and there are many tests which are not obvious in what they are testing.

Here's an example:

```python
# Before (unittest)
import asynctest
import asynctest.mock as mock

from opsdroid.cli.start import configure_lang
from opsdroid.core import OpsDroid
from opsdroid.events import Message
from opsdroid.matchers import match_regex
from opsdroid import constraints


class TestConstraints(asynctest.TestCase):
    """Test the opsdroid constraint decorators."""

    async def setUp(self):
        configure_lang({})

    async def getMockSkill(self):
        async def mockedskill(opsdroid, config, message):
            pass

        mockedskill.config = {}
        return mockedskill

    async def test_constrain_rooms_constrains(self):
        with OpsDroid() as opsdroid:
            skill = await self.getMockSkill()
            skill = match_regex(r".*")(skill)
            skill = constraints.constrain_rooms(["#general"])(skill)
            opsdroid.skills.append(skill)

            tasks = await opsdroid.parse(
                Message(text="Hello", user="user", target="#random", connector=None)
            )
            self.assertEqual(len(tasks), 2)  # Just match_always and match_event
```

```python
# After (pytest)
import pytest

from opsdroid.cli.start import configure_lang
from opsdroid.core import OpsDroid
from opsdroid.events import Message
from opsdroid.matchers import match_regex
from opsdroid import constraints


configure_lang({})  # Required for our internationalization of error messages


@pytest.fixture
def opsdroid():
    """An instance of the OpsDroid class."""
    with OpsDroid() as opsdroid:
        yield opsdroid


@pytest.fixture
def mock_skill():
    """A skill which does nothing but follows the skill API."""

    async def mockedskill(opsdroid, config, message):
        pass

    mockedskill.config = {}
    return mockedskill


@pytest.mark.asyncio
async def test_constrain_rooms_constrains(opsdroid, mock_skill):
    """Test that with the room constraint a skill is not called."""
    skill = match_regex(r".*")(mock_skill)
    skill = constraints.constrain_rooms(["#general"])(skill)
    opsdroid.skills.append(skill)

    tasks = await opsdroid.parse(
        Message(text="Hello", user="user", target="#random", connector=None)
    )
    assert len(tasks) == 2  # Just match_always and match_event
```

Fixtures such as the `opsdroid` fixture will be extremely useful in many tests, so we will be creating some standard ones like this which will be available in all tests. If you write a fixture that you think could be useful in other places please don't hesitate to contribute it to the `conftest.py` file.

If you have any questions you can chat to us on [Matrix](https://riot.im/app/#/room/#opsdroid-general:matrix.org). We encourage you to get started and if you have issues or get stuck then open a [draft PR](https://github.blog/2019-02-14-introducing-draft-pull-requests/) with your changes and we can take a look.

---

When this issue is complete the documentation about the migration should be removed from the testing docs.


Discussion / hints:
@jacobtomlinson Can I claim it?
@tarangchikhalia you are welcome to start converting some tests and raise a PR. As I explained in the issue there are many tests to convert. So it will take a while and I expect many people will work on it, nobody can specifically claim this issue.
Hi @tarangchikhalia , 
How is this going? Have you written started making some progress on this one?
I found this to be a good first issue for me to start contributing, and I do not want us to do the same thing twice. Please let me know of the modules that you have already worked on so that I can start with something else.

(If there is a different communication channel for questions like this, please direct me to the correct place).

Thanks.
@abhishekmadhu as the issue states please just search the codebase for old tests and convert them over. There are many to do and ideally I would like to see lots of small PRs rather than large ones converting many tests. That way folks don't have to worry about treading on each others toes.

> So if you wish to help in the effort you can start by searching the codebase for unittest suites. These are classes which are subclassed from `unittest.TestCase` or `asynctest.TestCase`, so searching all files for `unittest.TestCase` and `asynctest.TestCase` should be a good place to start.
Hi @jacobtomlinson, I am hoping to contribute on this project starting from migratings tests from unittest to pytest. I have tried my hands on simple database_test module and opened a pull request, however there is a codecoverage fail for that. Does it needs to be attending or I can move to other test modules? 
I'm happy to resolve the issue. Please move on to more tests. 
Hi, this is my first time contributing and I am eager to help, I tried to convert test_database_sqlite.py to a pytest in a draft request so any feedback on this will be greatly appreciated. Also I just wanted to mention that the "see the docs" link in the first comment leads to a page that does not exist.
Hello @rr3khan thank you for pointing that to us. The [contributing documentation](https://docs.opsdroid.dev/en/stable/contributing.html) is here.

I am off to work but I'll try to have a look at the PR when I come back 😄👍
Fixed the link above, thanks!

Fix patch:
diff --git a/opsdroid/connector/gitter/__init__.py b/opsdroid/connector/gitter/__init__.py
index e5174bb7e..d93739118 100644
--- a/opsdroid/connector/gitter/__init__.py
+++ b/opsdroid/connector/gitter/__init__.py
@@ -1,136 +1,1 @@
-"""A connector for Gitter."""
-import logging
-import aiohttp
-import asyncio
-import json
-import urllib
-from voluptuous import Required
-
-from opsdroid.connector import Connector, register_event
-from opsdroid.events import Message
-
-_LOGGER = logging.getLogger(__name__)
-GITTER_STREAM_API = "https://stream.gitter.im/v1/rooms"
-GITTER_MESSAGE_BASE_API = "https://api.gitter.im/v1/rooms"
-CURRENT_USER_API = "https://api.gitter.im/v1/user/me"
-CONFIG_SCHEMA = {Required("token"): str, Required("room-id"): str, "bot-name": str}
-
-
-class ConnectorGitter(Connector):
-    """A connector for Gitter."""
-
-    def __init__(self, config, opsdroid=None):
-        """Create the connector."""
-        super().__init__(config, opsdroid=opsdroid)
-        _LOGGER.debug(_("Starting Gitter Connector."))
-        self.name = "gitter"
-        self.bot_name = None  # set at connection time
-        self.session = None
-        self.response = None
-        self.room_id = self.config.get("room-id")
-        self.access_token = self.config.get("token")
-        self.update_interval = 1
-        self.opsdroid = opsdroid
-        self.listening = True
-
-    async def connect(self):
-        """Create the connection."""
-
-        # Create connection object with chat library
-
-        _LOGGER.debug(_("Connecting with Gitter stream."))
-        self.session = aiohttp.ClientSession()
-
-        # Gitter figures out who we are based on just our access token, but we
-        # need to additionally know our own user ID in order to know which
-        # messages comes from us.
-        current_user_url = self.build_url(
-            CURRENT_USER_API,
-            access_token=self.access_token,
-        )
-        response = await self.session.get(current_user_url, timeout=None)
-        # We cannot continue without a user ID, so raise if this failed.
-        response.raise_for_status()
-        response_json = await response.json()
-        self.bot_gitter_id = response_json["id"]
-        self.bot_name = response_json["username"]
-        _LOGGER.debug(
-            _("Successfully obtained bot's gitter id, %s."), self.bot_gitter_id
-        )
-
-        message_stream_url = self.build_url(
-            GITTER_STREAM_API,
-            self.room_id,
-            "chatMessages",
-            access_token=self.access_token,
-        )
-        self.response = await self.session.get(message_stream_url, timeout=None)
-        self.response.raise_for_status()
-
-    def build_url(self, base_url, *res, **params):
-        """Build the url. args ex:(base_url,p1,p2=1,p2=2)."""
-
-        url = base_url
-        for r in res:
-            url = "{}/{}".format(url, r)
-        if params:
-            url = "{}?{}".format(url, urllib.parse.urlencode(params))
-        return url
-
-    async def listen(self):
-        """Keep listing to the gitter channel."""
-        _LOGGER.debug(_("Listening with Gitter stream."))
-        while self.listening:
-            try:
-                await self._get_messages()
-            except AttributeError:
-                break
-
-    async def _get_messages(self):
-        """Message listener."""
-        await asyncio.sleep(self.update_interval)
-        async for data in self.response.content.iter_chunked(1024):
-            message = await self.parse_message(data)
-            # Do not parse messages that we ourselves sent.
-            if message is not None and message.user_id != self.bot_gitter_id:
-                await self.opsdroid.parse(message)
-
-    async def parse_message(self, message):
-        """Parse response from gitter to send message."""
-        message = message.decode("utf-8").rstrip("\r\n")
-        if len(message) > 1:
-            message = json.loads(message)
-            _LOGGER.debug(message)
-            try:
-                return Message(
-                    text=message["text"],
-                    user=message["fromUser"]["username"],
-                    user_id=message["fromUser"]["id"],
-                    target=self.room_id,
-                    connector=self,
-                )
-            except KeyError as err:
-                _LOGGER.error(_("Unable to parse message %r."), err)
-
-    @register_event(Message)
-    async def send_message(self, message):
-        """Received parsed message and send it back to gitter room."""
-        # Send message.text back to the chat service
-        url = self.build_url(GITTER_MESSAGE_BASE_API, message.target, "chatMessages")
-        headers = {
-            "Authorization": "Bearer " + self.access_token,
-            "Content-Type": "application/json",
-            "Accept": "application/json",
-        }
-        payload = {"text": message.text}
-        resp = await self.session.post(url, json=payload, headers=headers)
-        if resp.status == 200:
-            _LOGGER.info(_("Successfully responded."))
-        else:
-            _LOGGER.error(_("Unable to respond."))
-
-    async def disconnect(self):
-        """Disconnect the gitter."""
-        # Disconnect from the chat service
-        self.listening = False
-        await self.session.close()
+from .connector import ConnectorGitter  # noqa
diff --git a/opsdroid/connector/gitter/connector.py b/opsdroid/connector/gitter/connector.py
new file mode 100644
index 000000000..3f3656d09
--- /dev/null
+++ b/opsdroid/connector/gitter/connector.py
@@ -0,0 +1,143 @@
+"""A connector for Gitter."""
+import logging
+import aiohttp
+import asyncio
+import json
+import urllib
+from voluptuous import Required
+
+from opsdroid.connector import Connector, register_event
+from opsdroid.events import Message
+
+_LOGGER = logging.getLogger(__name__)
+GITTER_STREAM_API = "https://stream.gitter.im"
+GITTER_BASE_API = "https://api.gitter.im"
+CONFIG_SCHEMA = {
+    Required("token"): str,
+    Required("room-id"): str,
+    "bot-name": str,
+    "api-base-url": str,
+    "api-stream-url": str,
+    "update-interval": str,
+}
+
+
+class ConnectorGitter(Connector):
+    """A connector for Gitter."""
+
+    def __init__(self, config, opsdroid=None):
+        """Create the connector."""
+        super().__init__(config, opsdroid=opsdroid)
+        _LOGGER.debug(_("Starting Gitter Connector."))
+        self.name = "gitter"
+        self.bot_name = None  # set at connection time
+        self.session = None
+        self.response = None
+        self.room_id = self.config.get("room-id")
+        self.access_token = self.config.get("token")
+        self.gitter_base_api = self.config.get("api-base-url", GITTER_BASE_API)
+        self.gitter_stream_api = self.config.get("api-stream-url", GITTER_STREAM_API)
+        self.update_interval = self.config.get("update-interval", 1)
+        self.opsdroid = opsdroid
+        self.listening = True
+
+    async def connect(self):
+        """Create the connection."""
+
+        # Create connection object with chat library
+
+        _LOGGER.debug(_("Connecting with Gitter stream."))
+        self.session = aiohttp.ClientSession()
+
+        # Gitter figures out who we are based on just our access token, but we
+        # need to additionally know our own user ID in order to know which
+        # messages comes from us.
+        current_user_url = self.build_url(
+            f"{self.gitter_base_api}/v1/user/me",
+            access_token=self.access_token,
+        )
+        response = await self.session.get(current_user_url, timeout=None)
+        # We cannot continue without a user ID, so raise if this failed.
+        response.raise_for_status()
+        response_json = await response.json()
+        self.bot_gitter_id = response_json["id"]
+        self.bot_name = response_json["username"]
+        _LOGGER.debug(
+            _("Successfully obtained bot's gitter id, %s."), self.bot_gitter_id
+        )
+
+        message_stream_url = self.build_url(
+            f"{self.gitter_stream_api}/v1/rooms",
+            self.room_id,
+            "chatMessages",
+            access_token=self.access_token,
+        )
+        self.response = await self.session.get(message_stream_url, timeout=None)
+        self.response.raise_for_status()
+
+    def build_url(self, base_url, *res, **params):
+        """Build the url. args ex:(base_url,p1,p2=1,p2=2)."""
+
+        url = base_url
+        for r in res:
+            url = "{}/{}".format(url, r)
+        if params:
+            url = "{}?{}".format(url, urllib.parse.urlencode(params))
+        return url
+
+    async def listen(self):
+        """Keep listing to the gitter channel."""
+        _LOGGER.debug(_("Listening with Gitter stream."))
+        while self.listening:
+            await self._get_messages()
+
+    async def _get_messages(self):
+        """Message listener."""
+        await asyncio.sleep(self.update_interval)
+        async for data in self.response.content.iter_chunked(1024):
+            message = await self.parse_message(data)
+            # Do not parse messages that we ourselves sent.
+            if message is not None and message.user_id != self.bot_gitter_id:
+                await self.opsdroid.parse(message)
+
+    async def parse_message(self, message):
+        """Parse response from gitter to send message."""
+        message = message.decode("utf-8").rstrip("\r\n")
+        if len(message) > 1:
+            message = json.loads(message)
+            _LOGGER.debug(message)
+            try:
+                return Message(
+                    text=message["text"],
+                    user=message["fromUser"]["username"],
+                    user_id=message["fromUser"]["id"],
+                    target=self.room_id,
+                    connector=self,
+                )
+            except KeyError as err:
+                _LOGGER.error(_("Unable to parse message %r."), err)
+
+    @register_event(Message)
+    async def send_message(self, message):
+        """Received parsed message and send it back to gitter room."""
+        # Send message.text back to the chat service
+        url = self.build_url(
+            f"{self.gitter_base_api}/v1/rooms", message.target, "chatMessages"
+        )
+        headers = {
+            "Authorization": "Bearer " + self.access_token,
+            "Content-Type": "application/json",
+            "Accept": "application/json",
+        }
+        payload = {"text": message.text}
+        resp = await self.session.post(url, json=payload, headers=headers)
+        if resp.status == 200:
+            _LOGGER.info(_("Successfully responded."))
+        else:
+            _LOGGER.error(_("Unable to respond."))
+
+    async def disconnect(self):
+        """Disconnect the gitter."""
+        # Disconnect from the chat service
+        self.listening = False
+        await self.session.close()


Test patch:
diff --git a/opsdroid/connector/gitter/tests/responses/gitter_bot_message.json b/opsdroid/connector/gitter/tests/responses/gitter_bot_message.json
new file mode 100644
index 000000000..4235b1a0c
--- /dev/null
+++ b/opsdroid/connector/gitter/tests/responses/gitter_bot_message.json
@@ -0,0 +1,7 @@
+{
+    "text": "hi",
+    "fromUser": {
+        "username": "bot",
+        "id": "12345"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitter/tests/responses/gitter_connect.json b/opsdroid/connector/gitter/tests/responses/gitter_connect.json
new file mode 100644
index 000000000..6eaf01f17
--- /dev/null
+++ b/opsdroid/connector/gitter/tests/responses/gitter_connect.json
@@ -0,0 +1,5 @@
+{
+    "login": "opsdroid",
+    "id": "12345",
+    "username": "bot"
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitter/tests/responses/gitter_message.json b/opsdroid/connector/gitter/tests/responses/gitter_message.json
new file mode 100644
index 000000000..ccfd1ff50
--- /dev/null
+++ b/opsdroid/connector/gitter/tests/responses/gitter_message.json
@@ -0,0 +1,7 @@
+{
+    "text": "hi",
+    "fromUser": {
+        "username": "not a bot",
+        "id": "67890"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitter/tests/responses/gitter_send_message.json b/opsdroid/connector/gitter/tests/responses/gitter_send_message.json
new file mode 100644
index 000000000..3756ca87a
--- /dev/null
+++ b/opsdroid/connector/gitter/tests/responses/gitter_send_message.json
@@ -0,0 +1,23 @@
+{
+    "id": "5330521e20d939a3be000018",
+    "text": "Hello world",
+    "html": "Hello world",
+    "sent": "2014-03-24T15:41:18.991Z",
+    "editedAt": null,
+    "fromUser": {
+        "id": "5315ef029517002db7dde53b",
+        "username": "foo",
+        "displayName": "Mauro Pompilio",
+        "url": "/malditogeek",
+        "avatarUrlSmall": "https://avatars.githubusercontent.com/u/14751?",
+        "avatarUrlMedium": "https://avatars.githubusercontent.com/u/14751?",
+        "v": 2
+    },
+    "unread": false,
+    "readBy": 0,
+    "urls": [],
+    "mentions": [],
+    "issues": [],
+    "meta": {},
+    "v": 1
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitter/tests/test_connector_gitter.py b/opsdroid/connector/gitter/tests/test_connector_gitter.py
new file mode 100644
index 000000000..ca22b08fe
--- /dev/null
+++ b/opsdroid/connector/gitter/tests/test_connector_gitter.py
@@ -0,0 +1,158 @@
+"""Tests for the RocketChat class."""
+
+import logging
+import pytest
+from pathlib import Path
+
+
+from opsdroid.connector.gitter import ConnectorGitter
+from opsdroid.events import Message
+from opsdroid.matchers import match_regex
+from opsdroid.testing import running_opsdroid
+
+
+@pytest.fixture
+async def connector(opsdroid, mock_api_obj):
+    opsdroid.config["connectors"] = {
+        "gitter": {
+            "token": "abc123",
+            "room-id": "foo",
+            "api-base-url": mock_api_obj.base_url,
+            "api-stream-url": mock_api_obj.base_url,
+            "update-interval": 0.01,
+        }
+    }
+    await opsdroid.load()
+    return opsdroid.get_connector("gitter")
+
+
+def get_response_path(response):
+    return Path(__file__).parent / "responses" / response
+
+
+def test_init():
+    """Test that the connector is initialised properly."""
+    connector = ConnectorGitter(
+        {"bot-name": "github", "room-id": "test-id", "token": "test-token"}
+    )
+    assert connector.default_target is None
+    assert connector.name == "gitter"
+
+
+async def test_build_url(connector):
+    assert "test/api/test-id/chatMessages?access_token=token" == connector.build_url(
+        "test/api", "test-id", "chatMessages", access_token="token"
+    )
+
+
+@pytest.mark.asyncio
+async def test_parse_message(connector):
+    message = await connector.parse_message(
+        b'{"text":"hello", "fromUser":{"username":"testUSer", "id": "123"}}'
+    )
+    assert isinstance(message, Message)
+
+
+@pytest.mark.asyncio
+async def test_parse_message_key_error(connector, caplog):
+    await connector.parse_message(b'{"text":"hello"}')
+    assert "Unable to parse message" in caplog.text
+
+
+@pytest.mark.add_response(
+    "/v1/user/me", "GET", get_response_path("gitter_connect.json"), status=200
+)
+@pytest.mark.add_response(
+    "/v1/rooms/foo/chatMessages",
+    "GET",
+    get_response_path("gitter_message.json"),
+    status=200,
+)
+@pytest.mark.add_response(
+    "/v1/rooms/foo/chatMessages",
+    "POST",
+    get_response_path("gitter_send_message.json"),
+    status=200,
+)
+@pytest.mark.asyncio
+async def test_end_to_end(connector, opsdroid, mock_api, caplog):
+    """Connect, recieve a message, trigger a skill and respond.
+
+    - When opsdroid starts it will connect to gitter
+    - When the connector starts listening it will get a message
+    - That message should trigger the test_skill
+    - The skill will respond and we check that in the logs
+    """
+
+    caplog.set_level(logging.INFO)
+
+    @match_regex("hi")
+    async def test_skill(opsdroid, config, event):
+        assert event.text == "hi"
+        logging.getLogger(__name__).info("test_skill called")
+        await event.respond("hey there!")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        assert connector.listening
+        assert connector.bot_gitter_id == "12345"
+
+        assert "test_skill called" in caplog.text
+        assert "Successfully responded." in caplog.text
+
+    assert not connector.listening
+
+
+@pytest.mark.add_response(
+    "/v1/user/me", "GET", get_response_path("gitter_connect.json"), status=200
+)
+@pytest.mark.add_response(
+    "/v1/rooms/foo/chatMessages",
+    "GET",
+    get_response_path("gitter_message.json"),
+    status=200,
+)
+@pytest.mark.add_response(
+    "/v1/rooms/foo/chatMessages",
+    "POST",
+    get_response_path("gitter_send_message.json"),
+    status=400,
+)
+@pytest.mark.asyncio
+async def test_send_message_failure(connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+    await connector.connect()
+    await connector.send_message(
+        Message(text="Hello world", target="foo", user="bar", connector=connector)
+    )
+    assert "Unable to respond." in caplog.text
+
+
+@pytest.mark.add_response(
+    "/v1/user/me", "GET", get_response_path("gitter_connect.json"), status=200
+)
+@pytest.mark.add_response(
+    "/v1/rooms/foo/chatMessages",
+    "GET",
+    get_response_path("gitter_bot_message.json"),
+    status=200,
+)
+@pytest.mark.asyncio
+async def test_ignore_bot_message(connector, opsdroid, mock_api, caplog):
+    """Ignore messages from self.
+
+    If the ID in the ``/user/me`` call matches that in the message
+    the message should be ignored and the skill is not triggered.
+    """
+
+    caplog.set_level(logging.INFO)
+
+    @match_regex("hi")
+    async def test_skill(opsdroid, config, event):
+        logging.getLogger(__name__).info("test_skill called")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        assert "test_skill called" not in caplog.text
diff --git a/tests/test_connector_gitter.py b/tests/test_connector_gitter.py
deleted file mode 100644
index 027cce206..000000000
--- a/tests/test_connector_gitter.py
+++ /dev/null
@@ -1,178 +0,0 @@
-"""Tests for the RocketChat class."""
-
-import asyncio
-import json
-import unittest
-import asynctest
-import asynctest.mock as amock
-
-
-from opsdroid.cli.start import configure_lang
-from opsdroid.core import OpsDroid
-from opsdroid.connector.gitter import ConnectorGitter
-from opsdroid.events import Message
-
-
-class TestConnectorGitter(unittest.TestCase):
-    """Test the opsdroid github connector class."""
-
-    def setUp(self):
-        self.loop = asyncio.new_event_loop()
-
-    def test_init(self):
-        """Test that the connector is initialised properly."""
-        connector = ConnectorGitter(
-            {"bot-name": "github", "room-id": "test-id", "token": "test-token"}
-        )
-        self.assertEqual("gitter", connector.name)
-
-
-class TestConnectorGitterAsync(asynctest.TestCase):
-    """Test the async methods of the opsdroid github connector class."""
-
-    def setUp(self):
-        opsdroid = amock.CoroutineMock()
-        configure_lang({})
-        self.connector = ConnectorGitter(
-            {"bot-name": "github", "room-id": "test-id", "token": "test-token"},
-            opsdroid=opsdroid,
-        )
-        with amock.patch("aiohttp.ClientSession") as mocked_session:
-            self.connector.session = mocked_session
-
-    async def test_connect(self):
-        BOT_GITTER_ID = "12345"
-        with amock.patch("aiohttp.ClientSession.get") as patched_request:
-            mockresponse = amock.CoroutineMock()
-            mockresponse.status = 200
-            mockresponse.json = amock.CoroutineMock(
-                return_value={
-                    "login": "opsdroid",
-                    "id": BOT_GITTER_ID,
-                    "username": "bot",
-                }
-            )
-            patched_request.return_value = asyncio.Future()
-            patched_request.return_value.set_result(mockresponse)
-            await self.connector.connect()
-        assert self.connector.bot_gitter_id == BOT_GITTER_ID
-
-    def test_build_url(self):
-        self.assertEqual(
-            "test/api/test-id/chatMessages?access_token=token",
-            self.connector.build_url(
-                "test/api", "test-id", "chatMessages", access_token="token"
-            ),
-        )
-
-    async def test_parse_message(self):
-        await self.connector.parse_message(
-            b'{"text":"hello", "fromUser":{"username":"testUSer"}}'
-        )
-
-    async def test_parse_message_key_error(self):
-        await self.connector.parse_message(b'{"text":"hello"}')
-        self.assertLogs("_LOGGER", level="ERROR")
-
-    async def test_listen_loop(self):
-        """Test that listening consumes from the socket."""
-        connector = ConnectorGitter(
-            {"bot-name": "github", "room-id": "test-id", "token": "test-token"},
-            opsdroid=OpsDroid(),
-        )
-        connector._get_messages = amock.CoroutineMock()
-        connector._get_messages.side_effect = Exception()
-        with self.assertRaises(Exception):
-            await connector.listen()
-        self.assertTrue(connector._get_messages.called)
-
-    async def test_listen_break_loop(self):
-        """Test that listening consumes from the socket."""
-        connector = connector = ConnectorGitter(
-            {"bot-name": "github", "room-id": "test-id", "token": "test-token"},
-            opsdroid=OpsDroid(),
-        )
-        connector._get_messages = amock.CoroutineMock()
-        connector._get_messages.side_effect = AttributeError
-        await connector.listen()
-        self.assertTrue(connector._get_messages.called)
-
-    async def test_get_message(self):
-        """Test that listening consumes from the socket."""
-
-        BOT_GITTER_ID = "12345"
-        OTHER_GITTER_ID = "67890"
-
-        async def iter_chuncked1(n=None):
-            response = [
-                {
-                    "text": "hi",
-                    "fromUser": {"username": "not a bot", "id": OTHER_GITTER_ID},
-                },
-                {"text": "hi", "fromUser": {"username": "bot", "id": BOT_GITTER_ID}},
-                {
-                    "text": "hi",
-                    "fromUser": {"username": "not a bot", "id": OTHER_GITTER_ID},
-                },
-            ]
-            for doc in response:
-                yield json.dumps(doc).encode()
-
-        response1 = amock.CoroutineMock()
-        response1.content.iter_chunked = iter_chuncked1
-
-        connector = ConnectorGitter(
-            {"bot-name": "github", "room-id": "test-id", "token": "test-token"},
-            opsdroid=OpsDroid(),
-        )
-        # Connect first, in order to set bot_gitter_id.
-        with amock.patch("aiohttp.ClientSession.get") as patched_request:
-            mockresponse = amock.CoroutineMock()
-            mockresponse.status = 200
-            mockresponse.json = amock.CoroutineMock(
-                return_value={
-                    "login": "opsdroid",
-                    "id": BOT_GITTER_ID,
-                    "username": "bot",
-                }
-            )
-            patched_request.return_value = asyncio.Future()
-            patched_request.return_value.set_result(mockresponse)
-            await connector.connect()
-
-        connector.opsdroid.parse = amock.CoroutineMock()
-        connector.response = response1
-        assert await connector._get_messages() is None
-        # Should be called *twice* given these three messages (skipping own message)
-        assert connector.opsdroid.parse.call_count == 2
-
-    async def test_send_message_success(self):
-        post_response = amock.Mock()
-        post_response.status = 200
-
-        with OpsDroid(), amock.patch.object(
-            self.connector.session, "post"
-        ) as patched_request:
-            patched_request.return_value = asyncio.Future()
-            patched_request.return_value.set_result(post_response)
-            await self.connector.send_message(Message("", "", "", self.connector))
-
-    async def test_send_message_not_success(self):
-        post_response = amock.Mock()
-        post_response.status = 400
-
-        with OpsDroid(), amock.patch.object(
-            self.connector.session, "post"
-        ) as patched_request:
-            patched_request.return_value = asyncio.Future()
-            patched_request.return_value.set_result(post_response)
-            await self.connector.send_message(Message("", "", "", self.connector))
-
-    async def test_disconnect(self):
-        post_response = amock.Mock()
-        with OpsDroid(), amock.patch.object(
-            self.connector.session, "close"
-        ) as patched_request:
-            patched_request.return_value = asyncio.Future()
-            patched_request.return_value.set_result(post_response)
-            await self.connector.disconnect()