Repository: microsoft/Qcodes

Issue:
Slack API deprecating early methods breaks qcodes.utils.slack.Slack class
###
A couple of days ago, Slack deprecated some of its API:
[https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api](https://api.slack.com/changelog/2020-01-deprecating-antecedents-to-the-conversations-api)

Which breaks the get_im_ids method (and more probably).

The slacker package that is used by qcodes is no longer maintained and is archived:
[https://github.com/os/slacker/commit/3c7f43f75ee838be1121342c1aea1360388015e8](https://github.com/os/slacker/commit/3c7f43f75ee838be1121342c1aea1360388015e8)

Slack itself has a Python SDK:
[https://github.com/slackapi/python-slack-sdk](https://github.com/slackapi/python-slack-sdk)
which can replace the slacker package.

I will probably send in a PR next week.

### Steps to reproduce

import qcodes.utils.slack
bot_token = open(r'C:\Users\hbmeerwaldt\Desktop\qi_bot_token.txt').read()  
slacknames = ['haroldmeerwaldt']  
config = {'bot_name': 'QI','token': bot_token, 'names': slacknames}  
qcodes.utils.slack.Slack(config=config))
  


### Expected behaviour
A Slack instance is created successfully

### Actual behaviour
Traceback (most recent call last):
  File "C:\Users\hbmeerwaldt\QuTech\QuantumInspire\repositories\qubits-control\env\lib\site-packages\IPython\core\interactiveshell.py", line 3417, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-4-c8c171b87116>", line 1, in <module>
    qcodes.utils.slack.Slack(config=config)
  File "C:\Users\hbmeerwaldt\QuTech\QuantumInspire\repositories\qubits-control\env\lib\site-packages\qcodes\utils\slack.py", line 124, in __init__
    self.get_im_ids(self.users)
  File "C:\Users\hbmeerwaldt\QuTech\QuantumInspire\repositories\qubits-control\env\lib\site-packages\qcodes\utils\slack.py", line 235, in get_im_ids
    response = self.slack.im.list()
  File "C:\Users\hbmeerwaldt\QuTech\QuantumInspire\repositories\qubits-control\env\lib\site-packages\slacker\__init__.py", line 595, in list
    return self.get('im.list')
  File "C:\Users\hbmeerwaldt\QuTech\QuantumInspire\repositories\qubits-control\env\lib\site-packages\slacker\__init__.py", line 118, in get
    return self._request(
  File "C:\Users\hbmeerwaldt\QuTech\QuantumInspire\repositories\qubits-control\env\lib\site-packages\slacker\__init__.py", line 102, in _request
    raise Error(response.error)
slacker.Error: method_deprecated

### System
Windows 10 Enterprise

master

7c5dca3956





Discussion:
@haroldmeerwaldt hey, thanks for catching this! and thank you for the intention to submit a PR!

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -394,7 +394,7 @@
 # there when generating the docs
 autodoc_mock_imports = ['pyspcm', 'zhinst', 'zhinst.utils', 'keysightSD1',
                         'cffi', 'spirack', 'clr', 'win32com',
-                        'win32com.client', 'pythoncom', 'slacker', 'hickle']
+                        'win32com.client', 'pythoncom', 'slack-sdk', 'hickle']
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
diff --git a/qcodes/utils/slack.py b/qcodes/utils/slack.py
--- a/qcodes/utils/slack.py
+++ b/qcodes/utils/slack.py
@@ -3,12 +3,22 @@
 Some default commands are provided, and custom commands/tasks can be
 attached (see below).
 
-To setup the Slack bot, a bot first has to be registered via Slack
-by clicking 'creating a new bot user' on https://api.slack.com/bot-users.
-Once registered, the bot will have a name and unique token.
+To setup the Slack bot, a bot first has to be created via Slack
+by clicking 'Create New App' on https://api.slack.com/apps.
+Once created, the bot will have a name and unique token.
 These and other settings have to be saved in a config dict (see init( or
 Parameters) in :class:`Slack`).
 
+The App containing your bot needs to have the following bot token scopes to
+perform all methods successfully:
+- channels:history
+- channels:read
+- chat:write
+- files:write
+- users:read
+These can be set after clicking OAuth & Permissions in the left menubar after
+selecting your bot at https://api.slack.com/apps (or during creation).
+
 Communication with the Slack bot is performed via instant messaging.
 When an IM is sent to the Slack bot, it will be processed during the next
 `update()` call (provided the username is registered in the config).
@@ -32,24 +42,24 @@
 performed every time `update()` is called.
 """
 
-import os
-import tempfile
-from functools import partial
-from time import sleep
 import inspect
-from slacker import Slacker
-import warnings
 import logging
+import os
+import tempfile
 import threading
 import traceback
+import warnings
+from functools import partial
+from time import sleep
+
 from requests.exceptions import ReadTimeout, HTTPError, ConnectTimeout
 from requests.packages.urllib3.exceptions import ReadTimeoutError
+from slack_sdk import WebClient
 
-
-from qcodes.plots.base import BasePlot
 from qcodes import config as qc_config
 from qcodes.instrument.parameter import _BaseParameter
 from qcodes.loops import active_loop, active_data_set
+from qcodes.plots.base import BasePlot
 
 
 class SlackTimeoutWarning(UserWarning):
@@ -118,8 +128,7 @@ def __init__(self, interval=3, config=None, auto_start=True, **commands):
         else:
             self.config = qc_config.user.slack
 
-        self.slack = Slacker(self.config['token'])
-        self.bot_id = self.slack.users.get_user_id(self.config['bot_name'])
+        self.slack = WebClient(token=self.config['token'])
         self.users = self.get_users(self.config['names'])
         self.get_im_ids(self.users)
 
@@ -196,11 +205,7 @@ def user_from_id(self, user_id):
         Returns:
             dict: User information.
         """
-        users = [user for user in self.users if
-                 self.users[user]['id'] == user_id]
-        assert len(users) == 1, "Could not find unique user with id {}".format(
-            user_id)
-        return users[0]
+        return self.slack.users_info(user=user_id)['user']
 
     def get_users(self, usernames):
         """
@@ -212,8 +217,8 @@ def get_users(self, usernames):
             dict: {username: user}
         """
         users = {}
-        response = self.slack.users.list()
-        for member in response.body['members']:
+        response = self.slack.users_list()
+        for member in response['members']:
             if member['name'] in usernames:
                 users[member['name']] = member
         if len(users) != len(usernames):
@@ -232,14 +237,14 @@ def get_im_ids(self, users):
         Returns:
             None.
         """
-        response = self.slack.im.list()
-        user_ids = {user: users[user]['id'] for user in users}
-        im_ids = {im['user']: im['id'] for im in response.body['ims']}
+        response = self.slack.conversations_list(types='im')
+        user_ids = {username: user['id'] for username, user in users.items()}
+        im_ids = {chan['user']: chan['id'] for chan in response['channels']}
         for username, user_id in user_ids.items():
-            if user_id in im_ids:
+            if user_id in im_ids.keys():
                 users[username]['im_id'] = im_ids[user_id]
                 # update last ts
-                messages = self.get_im_messages(username=username, count=1)
+                messages = self.get_im_messages(username=username, limit=1)
                 if messages:
                     users[username]['last_ts'] = float(messages[0]['ts'])
                 else:
@@ -255,13 +260,19 @@ def get_im_messages(self, username, **kwargs):
         Returns:
             List of IM messages.
         """
+        # provide backward compatibility with 'count' keyword. It still works,
+        # but is undocumented. 'count' likely does the same as 'limit', but
+        # 'limit' takes precedence
+        if 'limit' not in kwargs.keys():
+            kwargs['limit'] = kwargs.pop('count', None)
+
         channel = self.users[username].get('im_id', None)
         if channel is None:
             return []
         else:
-            response = self.slack.im.history(channel=channel,
-                                             **kwargs)
-            return response.body['messages']
+            response = self.slack.conversations_history(channel=channel,
+                                                        **kwargs)
+            return response['messages']
 
     def get_new_im_messages(self):
         """
@@ -300,11 +311,11 @@ def update(self):
         new_messages = {}
         try:
             new_messages = self.get_new_im_messages()
-        except (ReadTimeout, HTTPError, ConnectTimeout, ReadTimeoutError) as ex:
+        except (ReadTimeout, HTTPError, ConnectTimeout, ReadTimeoutError) as e:
             # catch any timeouts caused by network delays
             warnings.warn('error retrieving slack messages',
                           SlackTimeoutWarning)
-            logging.info(ex)
+            logging.info(e)
         self.handle_messages(new_messages)
 
     def help_message(self):
@@ -331,7 +342,7 @@ def handle_messages(self, messages):
                         msg += f' {args}'
                     if kwargs:
                         msg += f' {kwargs}'
-                    self.slack.chat.post_message(text=msg, channel=channel)
+                    self.slack.chat_postMessage(text=msg, channel=channel)
 
                     func = self.commands[command]
                     try:
@@ -348,16 +359,16 @@ def handle_messages(self, messages):
                             results = func(*args, **kwargs)
 
                         if results is not None:
-                            self.slack.chat.post_message(
+                            self.slack.chat_postMessage(
                                 text=f'Results: {results}',
                                 channel=channel)
 
-                    except:
-                        self.slack.chat.post_message(
+                    except Exception:
+                        self.slack.chat_postMessage(
                             text=f'Error: {traceback.format_exc()}',
                             channel=channel)
                 else:
-                    self.slack.chat.post_message(
+                    self.slack.chat_postMessage(
                         text='Command {} not understood. Try `help`'.format(
                             command),
                         channel=channel)
@@ -375,13 +386,13 @@ def add_task(self, command, *args, channel, **kwargs):
             None.
         """
         if command in self.task_commands:
-            self.slack.chat.post_message(
+            self.slack.chat_postMessage(
                 text=f'Added task "{command}"',
                 channel=channel)
             func = self.task_commands[command]
             self.tasks.append(partial(func, *args, channel=channel, **kwargs))
         else:
-            self.slack.chat.post_message(
+            self.slack.chat_postMessage(
                 text=f'Task command {command} not understood',
                 channel=channel)
 
@@ -406,11 +417,11 @@ def upload_latest_plot(self, channel, **kwargs):
             # Saves latest plot to filename
             latest_plot.save(filename=temp_filename)
             # Upload plot to slack
-            self.slack.files.upload(temp_filename, channels=channel)
+            self.slack.files_upload(file=temp_filename, channels=channel)
             os.remove(temp_filename)
         else:
-            self.slack.chat.post_message(text='No latest plot',
-                                         channel=channel)
+            self.slack.chat_postMessage(text='No latest plot',
+                                        channel=channel)
 
     def print_measurement_information(self, channel, **kwargs):
         """
@@ -427,14 +438,14 @@ def print_measurement_information(self, channel, **kwargs):
         """
         dataset = active_data_set()
         if dataset is not None:
-            self.slack.chat.post_message(
+            self.slack.chat_postMessage(
                 text='Measurement is {:.0f}% complete'.format(
                     100 * dataset.fraction_complete()),
                 channel=channel)
-            self.slack.chat.post_message(
+            self.slack.chat_postMessage(
                 text=repr(dataset), channel=channel)
         else:
-            self.slack.chat.post_message(
+            self.slack.chat_postMessage(
                 text='No latest dataset found',
                 channel=channel)
 
@@ -449,7 +460,7 @@ def check_msmt_finished(self, channel, **kwargs):
             bool: True if measurement is finished, False otherwise.
         """
         if active_loop() is None:
-            self.slack.chat.post_message(
+            self.slack.chat_postMessage(
                 text='Measurement complete',
                 channel=channel)
             return True


Test patch:
diff --git a/qcodes/tests/test_slack.py b/qcodes/tests/test_slack.py
--- a/qcodes/tests/test_slack.py
+++ b/qcodes/tests/test_slack.py
@@ -1,28 +1,362 @@
-def test_slack(mocker):
+from unittest.mock import call
 
+import pytest
+from requests.exceptions import ReadTimeout, HTTPError, ConnectTimeout
+from urllib3.exceptions import ReadTimeoutError
+
+from qcodes import Parameter
+
+
+class AnyStringWith(str):
+    def __eq__(self, other):
+        return self in other
+
+
+@pytest.fixture(name='mock_webclient', autouse=True)
+def setup_webclient(mocker):
+    mock_slack_sdk_module = mocker.MagicMock(name='slack_sdk_module')
+    mock_webclient = mocker.MagicMock(name='WebclientMock')
+    mock_slack_sdk_module.WebClient = mocker.MagicMock()
+    mock_slack_sdk_module.WebClient.return_value = mock_webclient
+    mocker.patch.dict('sys.modules', slack_sdk=mock_slack_sdk_module)
+
+    response = {'members': [{'name': 'dummyuser', 'id': 'DU123'}]}
+    mock_webclient.users_list.return_value = response
+
+    def mock_conversations_list(types):
+        if 'im' in types.split(','):
+            return {'channels': [{'user': 'DU123', 'id': 'CH234'}]}
+        else:
+            return None
+
+    mock_webclient.conversations_list.side_effect = mock_conversations_list
+
+    return mock_webclient
+
+
+@pytest.fixture(name='slack')
+def slack_fixture():
+    return setup_slack()
+
+
+def setup_slack():
+    slack_config = {
+        'bot_name': 'bot',
+        'token': '123',
+        'names': ['dummyuser']
+    }
+    import qcodes.utils.slack  # pylint: disable=import-outside-toplevel
+    slack = qcodes.utils.slack.Slack(config=slack_config, auto_start=False)
+
+    return slack
+
+
+def test_convert_command_should_convert_floats():
+    import qcodes.utils.slack  # pylint: disable=import-outside-toplevel
+    cmd, arg, kwarg = qcodes.utils.slack.convert_command('comm 0.234 key=0.1')
+    assert cmd == 'comm'
+    assert arg == [pytest.approx(0.234)]
+    assert kwarg == {'key': pytest.approx(0.1)}
+
+
+def test_slack_instance_should_contain_supplied_usernames(slack):
+    assert 'dummyuser' in slack.users.keys()
+
+
+def test_slack_instance_should_get_config_from_qc_config():
+    from qcodes import config as cf  # pylint: disable=import-outside-toplevel
     slack_config = {
         'bot_name': 'bot',
         'token': '123',
         'names': ['dummyuser']
     }
+    cf.add(key='slack', value=slack_config)
+    import qcodes.utils.slack  # pylint: disable=import-outside-toplevel
+    slack = qcodes.utils.slack.Slack(config=None, auto_start=False)
+    assert 'dummyuser' in slack.users.keys()
+
+
+def test_slack_instance_should_start(mocker):
+    slack_config = {
+        'bot_name': 'bot',
+        'token': '123',
+        'names': ['dummyuser']
+    }
+    mock_thread_start = mocker.patch('threading.Thread.start')
+    import qcodes.utils.slack  # pylint: disable=import-outside-toplevel
+    _ = qcodes.utils.slack.Slack(config=slack_config)
+
+    mock_thread_start.assert_called()
+
+
+def test_slack_instance_should_not_start_when_already_started(mocker):
+    slack_config = {
+        'bot_name': 'bot',
+        'token': '123',
+        'names': ['dummyuser']
+    }
+    mock_thread_start = mocker.patch('threading.Thread.start')
+    mock_thread_start.side_effect = RuntimeError
+
+    import qcodes.utils.slack  # pylint: disable=import-outside-toplevel
+    _ = qcodes.utils.slack.Slack(config=slack_config)
+
+    mock_thread_start.assert_called()
+
+
+def test_slack_instance_should_start_and_stop(mocker):
+    slack_config = {
+        'bot_name': 'bot',
+        'token': '123',
+        'names': ['dummyuser']
+    }
+    mocker.patch('threading.Thread.start')
+
+    import qcodes.utils.slack  # pylint: disable=import-outside-toplevel
+    slack = qcodes.utils.slack.Slack(config=slack_config, interval=0)
+    slack.stop()
+
+    assert not slack._is_active
+
+
+def test_slack_instance_should_return_username_from_id(mock_webclient, slack):
+    def mock_users_info(user):
+        if user == 'DU123':
+            return {'user': {'name': 'dummyuser', 'id': 'DU123'}}
+        else:
+            return None
+
+    mock_webclient.users_info.side_effect = mock_users_info
+
+    assert {'name': 'dummyuser', 'id': 'DU123'} == slack.user_from_id('DU123')
+
+
+def test_slack_instance_should_get_im_ids(mock_webclient):
+    def conversations_history(channel, limit=None):
+        if channel == 'CH234':
+            messages = [{'user': 'DU123', 'text': f'm{i}',
+                         'ts': f'{45.5 + i}'} for i in range(limit)]
+            response = {'messages': messages}
+            return response
+        else:
+            return None
+
+    mock_webclient.conversations_history.side_effect = conversations_history
+
+    slack = setup_slack()
+
+    assert slack.users['dummyuser']['im_id'] == 'CH234'
+    assert slack.users['dummyuser']['last_ts'] == pytest.approx(45.5)
+
+
+def test_slack_instance_should_get_im_ids_with_zero_messages(mock_webclient):
+    def conversations_history(channel, limit=None):
+        if channel == 'CH234':
+            response = {'messages': []}
+            return response
+        else:
+            return None
+
+    mock_webclient.conversations_history.side_effect = conversations_history
+    slack = setup_slack()
+
+    assert slack.users['dummyuser']['last_ts'] is None
+
+
+def test_slack_instance_should_get_im_messages_w_count(slack, mock_webclient):
+    def conversations_history(channel, limit=None):
+        if channel == 'CH234':
+            messages = [{'user': 'DU123', 'text': f'message{i}'}
+                        for i in range(limit)]
+            response = {'messages': messages}
+            return response
+        else:
+            return None
+
+    mock_webclient.conversations_history.side_effect = conversations_history
+
+    messages = slack.get_im_messages('dummyuser', count=3)
+    assert len(messages) == 3
+
+
+def test_slack_instance_should_get_im_messages_without_channel(mock_webclient):
+    def conversations_history(channel, limit=None):
+        if channel == 'CH234':
+            messages = [{'user': 'DU123', 'text': f'm{i}',
+                         'ts': f'{45.5 + i}'} for i in range(limit)]
+            response = {'messages': messages}
+            return response
+        else:
+            return None
+
+    mock_webclient.conversations_history.side_effect = conversations_history
+
+    def mock_conversations_list(types):
+        if 'im' in types.split(','):
+            return {'channels': []}
+        else:
+            return None
+
+    mock_webclient.conversations_list.side_effect = mock_conversations_list
+
+    slack = setup_slack()
+
+    messages = slack.get_im_messages('dummyuser')
+    assert len(messages) == 0
+
+
+def test_slack_instance_should_get_new_im_messages(mock_webclient):
+    def generator_function():
+        total = 8
+        new = 3
+        while True:
+            new_messages = [{'user': 'DU123', 'text': f'm{i}',
+                             'ts': f'{45.5 + i}'} for i in range(total)][-new:]
+            response = {'messages': new_messages}
+            yield response
+            total += new
+
+    generator = generator_function()
+
+    def conversations_history(channel, limit=None, oldest=None):
+        if channel == 'CH234':
+            return next(generator)
+        else:
+            return None
+
+    mock_webclient.conversations_history.side_effect = conversations_history
+
+    slack = setup_slack()
+
+    new_messages = slack.get_new_im_messages()
+    assert len(new_messages['dummyuser']) == 3
+
+
+def test_slack_instance_should_update(slack):
+    slack.update()
+    assert slack.tasks == []
+
+
+def test_slack_instance_should_update_with_task_returning_false(slack):
+    slack.add_task('finished', channel='CH234')
+    slack.update()
+    assert slack.tasks == []
+
+
+def test_slack_instance_should_update_with_task_returning_true(slack, mocker):
+    mocker.patch('qcodes.utils.slack.active_loop', return_value=not None)
+
+    slack.add_task('finished', channel='CH234')
+    slack.update()
+    task_added = slack.tasks[-1]
+
+    assert 'Slack.check_msmt_finished' in str(task_added.func)
+
+
+def test_slack_instance_should_update_with_exception(slack, mocker):
+    method_name = 'qcodes.utils.slack.Slack.get_new_im_messages'
+    mock_get_new_im_messages = mocker.patch(method_name)
+    mocker.patch('warnings.warn')
+    mocker.patch('logging.info')
+
+    for exception in [ReadTimeout, HTTPError, ConnectTimeout,
+                      ReadTimeoutError('pool', 'url', 'message')]:
+        mock_get_new_im_messages.side_effect = exception
+        slack.update()
+        assert slack.tasks == []
+
+
+def test_slack_instance_should_give_help_message(slack):
+    message = slack.help_message()
+    expected_message = '\nAvailable commands: `plot`, `msmt`, ' \
+                       '`measurement`, `notify`, `help`, `task`'
+    assert message == expected_message
+
+
+def test_slack_instance_should_handle_messages(mock_webclient, slack):
+    messages = {'dummyuser': [{'user': 'DU123', 'text': 'help'}]}
+    slack.handle_messages(messages)
+    expected_text = 'Results: \nAvailable commands: `plot`, ' \
+                    '`msmt`, `measurement`, `notify`, `help`, `task`'
+    expected_output = {'channel': 'CH234',
+                       'text': expected_text}
+
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_handle_messages_w_args_kw(mock_webclient, slack):
+    text = 'task finished key=1'
+    messages = {'dummyuser': [{'user': 'DU123', 'text': text}]}
+    slack.handle_messages(messages)
+    expected_output = {'channel': 'CH234', 'text': 'Added task "finished"'}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_handle_messages_w_parameter(mock_webclient, slack):
+    slack.commands.update({'comm': Parameter(name='param')})
+    messages = {'dummyuser': [{'user': 'DU123', 'text': 'comm'}]}
+    slack.handle_messages(messages)
+    expected_output = {'channel': 'CH234', 'text': 'Executing comm'}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_handle_messages_w_exception(mock_webclient, slack):
+    messages = {'dummyuser': [{'user': 'DU123', 'text': 'help toomany'}]}
+    slack.handle_messages(messages)
+    text = 'TypeError: help_message() takes 1 positional argument ' \
+           'but 2 were given\n'
+    expected_output = {'channel': 'CH234', 'text': AnyStringWith(text)}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_handle_messages_w_unkn_cmd(mock_webclient, slack):
+    messages = {'dummyuser': [{'user': 'DU123', 'text': 'comm'}]}
+    slack.handle_messages(messages)
+    text = 'Command comm not understood. Try `help`'
+    expected_output = {'channel': 'CH234', 'text': text}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_add_unknown_task_command(mock_webclient, slack):
+    slack.add_task('tcomm', channel='CH234')
+    text = 'Task command tcomm not understood'
+    expected_output = {'channel': 'CH234', 'text': text}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_upload_latest_plot(mock_webclient, slack, mocker):
+    method_name = 'qcodes.utils.slack.BasePlot.latest_plot'
+    mocker.patch(method_name, return_value=not None)
+    mocker.patch('os.remove')
+    slack.upload_latest_plot(channel='CH234')
+    expected_output = {'channels': 'CH234', 'file': AnyStringWith('.jpg')}
+    mock_webclient.files_upload.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_not_fail_upl_latest_wo_plot(mock_webclient, slack):
+    slack.upload_latest_plot(channel='CH234')
+    expected_output = {'channel': 'CH234', 'text': 'No latest plot'}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)
+
+
+def test_slack_inst_should_print_measurement(mock_webclient, slack, mocker):
+    dataset = mocker.MagicMock()
+    dataset.fraction_complete.return_value = 0.123
+    mocker.patch('qcodes.utils.slack.active_data_set', return_value=dataset)
+
+    slack.print_measurement_information(channel='CH234')
+
+    print(mock_webclient.chat_postMessage.calls)
+
+    text1 = 'Measurement is {:.0f}% complete'.format(0.123 * 100)
+    expected_out1 = {'channel': 'CH234', 'text': text1}
+    expected_out2 = {'channel': 'CH234', 'text': AnyStringWith('MagicMock')}
+    actual = mock_webclient.chat_postMessage.call_args_list
+    expected = [call(**expected_out1), call(**expected_out2)]
+    assert actual == expected
+
 
-    mock_slacker_module = mocker.MagicMock(name='slacker_module')
-    slacker_class_mock = mocker.MagicMock(name='SlackerMock')
-    slacker_class_mock.users.get_user_id = mocker.MagicMock(
-        return_value='bot_id'
-    )
-    response = mocker.MagicMock()
-    response.body = {'members': [{'name': 'dummyuser', 'id': '123'}]}
-    slacker_class_mock.users.list = mocker.MagicMock(
-        name='mock_users_list',
-        return_value=response
-    )
-    mock_slacker_module.Slacker = slacker_class_mock
-    mock_slacker_module.Slacker = mocker.MagicMock(
-        return_value=slacker_class_mock
-    )
-
-    mocker.patch.dict('sys.modules', slacker=mock_slacker_module)
-    from qcodes.utils.slack import Slack
-    slack = Slack(config=slack_config, auto_start=False)
-    assert 'dummyuser' in slack.users
+def test_slack_inst_should_print_measurement_wo_latest(mock_webclient, slack):
+    slack.print_measurement_information(channel='CH234')
+    expected_output = {'channel': 'CH234', 'text': 'No latest dataset found'}
+    mock_webclient.chat_postMessage.assert_called_with(**expected_output)