Repository: line/line-bot-sdk-python
Pull request #185 (2019-06-20 07:17:33)

Polish document


Patch:
diff --git a/linebot/models/events.py b/linebot/models/events.py
index 0eb4b98..90bd3ca 100644
--- a/linebot/models/events.py
+++ b/linebot/models/events.py
@@ -36,7 +36,7 @@ from linebot.models.things import (
     DeviceLink,
     ScenarioResult,
 )
-from linebot.models.things import Things  # noqa
+from linebot.models.things import Things  # noqa, backward compatibility
 
 
 class Event(with_metaclass(ABCMeta, Base)):
diff --git a/linebot/models/messages.py b/linebot/models/messages.py
index f541d29..9b719ad 100644
--- a/linebot/models/messages.py
+++ b/linebot/models/messages.py
@@ -74,7 +74,7 @@ class ImageMessage(Message):
         :param str id: Message ID
         :param content_provider: ContentProvider object
         :type content_provider:
-            :py:class:`linebot.models.messages.ContentProvider` | dict[str, str]
+            :py:class:`linebot.models.messages.ContentProvider`
         :param kwargs:
         """
         super(ImageMessage, self).__init__(id=id, **kwargs)
@@ -101,7 +101,7 @@ class VideoMessage(Message):
         :param long duration: Length of video file (milliseconds)
         :param content_provider: ContentProvider object
         :type content_provider:
-            :py:class:`linebot.models.messages.ContentProvider` | dict[str, str]
+            :py:class:`linebot.models.messages.ContentProvider`
         :param kwargs:
         """
         super(VideoMessage, self).__init__(id=id, **kwargs)
@@ -129,7 +129,7 @@ class AudioMessage(Message):
         :param long duration: Length of audio file (milliseconds)
         :param content_provider: ContentProvider object
         :type content_provider:
-            :py:class:`linebot.models.messages.ContentProvider` | dict[str, str]
+            :py:class:`linebot.models.messages.ContentProvider`
         :param kwargs:
         """
         super(AudioMessage, self).__init__(id=id, **kwargs)
diff --git a/linebot/models/template.py b/linebot/models/template.py
index 83b93c8..00da4ef 100644
--- a/linebot/models/template.py
+++ b/linebot/models/template.py
@@ -81,7 +81,7 @@ class ButtonsTemplate(Template):
     def __init__(self, text=None, title=None, thumbnail_image_url=None,
                  image_aspect_ratio=None,
                  image_size=None, image_background_color=None,
-                 actions=None, **kwargs):
+                 actions=None, default_action=None, **kwargs):
         """__init__ method.
 
         :param str text: Message text.
@@ -105,6 +105,9 @@ class ButtonsTemplate(Template):
         :param actions: Action when tapped.
             Max: 4
         :type actions: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param default_action: Action when image is tapped;
+            set for the entire image, title, and text area
+        :type default_action: T <= :py:class:`linebot.models.actions.Action`
         :param kwargs:
         """
         super(ButtonsTemplate, self).__init__(**kwargs)
@@ -117,6 +120,7 @@ class ButtonsTemplate(Template):
         self.image_size = image_size
         self.image_background_color = image_background_color
         self.actions = get_actions(actions)
+        self.default_action = get_action(default_action)
 
 
 class ConfirmTemplate(Template):
@@ -214,8 +218,9 @@ class CarouselColumn(Base):
     https://developers.line.biz/en/reference/messaging-api/#column-object
     """
 
-    def __init__(self, text=None, title=None, thumbnail_image_url=None,
-                 image_background_color=None, actions=None, **kwargs):
+    def __init__(self, text=None, title=None,
+                 thumbnail_image_url=None, image_background_color=None,
+                 actions=None, default_action=None, **kwargs):
         """__init__ method.
 
         :param str text: Message text.
@@ -230,6 +235,9 @@ class CarouselColumn(Base):
         :param actions: Action when tapped.
             Max: 3
         :type actions: list[T <= :py:class:`linebot.models.actions.Action`]
+        :param default_action: Action when image is tapped;
+            set for the entire image, title, and text area
+        :type default_action: T <= :py:class:`linebot.models.actions.Action`
         :param kwargs:
         """
         super(CarouselColumn, self).__init__(**kwargs)
@@ -239,6 +247,7 @@ class CarouselColumn(Base):
         self.thumbnail_image_url = thumbnail_image_url
         self.image_background_color = image_background_color
         self.actions = get_actions(actions)
+        self.default_action = get_action(default_action)
 
 
 class ImageCarouselColumn(Base):


Test patch:
diff --git a/tests/api/test_get_message_quota.py b/tests/api/test_get_message_quota.py
new file mode 100644
index 0000000..8192e9a
--- /dev/null
+++ b/tests/api/test_get_message_quota.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+import responses
+
+from linebot import (
+    LineBotApi
+)
+
+
+class TestLineBotApi(unittest.TestCase):
+    def setUp(self):
+        self.tested = LineBotApi('channel_secret')
+
+    @responses.activate
+    def test_get_message_quota(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/quota',
+            json={
+                'type': 'limited',
+                'value': 1000
+            },
+            status=200
+        )
+        res = self.tested.get_message_quota()
+        request = responses.calls[0].request
+        self.assertEqual('GET', request.method)
+        self.assertEqual('limited', res.type)
+        self.assertEqual(1000, res.value)
+
+    @responses.activate
+    def test_get_message_quota_2(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/quota',
+            json={'type': 'none'},
+            status=200
+        )
+        res = self.tested.get_message_quota()
+        request = responses.calls[0].request
+        self.assertEqual('GET', request.method)
+        self.assertEqual('none', res.type)
+
+    @responses.activate
+    def test_get_message_quota_consumption(self):
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/message/quota/consumption',
+            json={'totalUsage': 500},
+            status=200
+        )
+        res = self.tested.get_message_quota_consumption()
+        request = responses.calls[0].request
+        self.assertEqual('GET', request.method)
+        self.assertEqual(500, res.total_usage)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/api/test_rich_menu.py b/tests/api/test_rich_menu.py
index 383758f..b770c63 100644
--- a/tests/api/test_rich_menu.py
+++ b/tests/api/test_rich_menu.py
@@ -388,6 +388,56 @@ class TestLineBotApi(unittest.TestCase):
             LineBotApi.DEFAULT_API_ENDPOINT + '/v2/bot/user/all/richmenu'
         )
 
+    @responses.activate
+    def test_get_rich_menu_image(self):
+        rich_menu_id = '1234'
+        body = b'hogieoidksk'
+        responses.add(
+            responses.GET,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            body=body, status=200
+        )
+
+        res = self.tested.get_rich_menu_image(rich_menu_id)
+
+        request = responses.calls[0].request
+        self.assertEqual(request.method, 'GET')
+        self.assertEqual(
+            request.url,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+        )
+        self.assertEqual(
+            body,
+            res.content
+        )
+
+    @responses.activate
+    def test_set_rich_menu_image(self):
+        rich_menu_id = '1234'
+        body = b'hogieoidksk'
+        responses.add(
+            responses.POST,
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            json={}, status=200
+        )
+
+        self.tested.set_rich_menu_image(
+            rich_menu_id=rich_menu_id,
+            content_type='image/jpeg',
+            content=body
+        )
+
+        request = responses.calls[0].request
+        self.assertEqual('POST', request.method)
+        self.assertEqual(
+            LineBotApi.DEFAULT_API_ENDPOINT +
+            '/v2/bot/richmenu/{rich_menu_id}/content'.format(rich_menu_id=rich_menu_id),
+            request.url
+        )
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/models/serialize_test_case.py b/tests/models/serialize_test_case.py
new file mode 100644
index 0000000..a9dee1e
--- /dev/null
+++ b/tests/models/serialize_test_case.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import sys
+import unittest
+from numbers import Number
+
+from linebot.models import (
+    Base,
+)
+from linebot.utils import to_camel_case
+
+PY3 = sys.version_info[0] == 3
+
+
+class SerializeTestCase(unittest.TestCase):
+    MESSAGE = 'message'
+    STICKER = 'sticker'
+    POSTBACK = 'postback'
+    CAMERA = 'camera'
+    CAMERA_ROLL = 'cameraRoll'
+    DATETIMEPICKER = 'datetimepicker'
+    URI = 'uri'
+    LOCATION = 'location'
+    FLEX = 'flex'
+    SPACER = 'spacer'
+    BUBBLE = 'bubble'
+    CAROUSEL = 'carousel'
+    BOX = 'box'
+    BUTTON = 'button'
+    FILLER = 'filler'
+    ICON = 'icon'
+    TEXT = 'text'
+    IMAGE = 'image'
+    VIDEO = 'video'
+    AUDIO = 'audio'
+    SEPARATOR = 'separator'
+    IMAGEMAP = 'imagemap'
+    ACTION = 'action'
+    TEMPLATE = 'template'
+    BUTTONS = 'buttons'
+    CONFIRM = 'confirm'
+    IMAGE_CAROUSEL = 'image_carousel'
+
+    def serialize_as_dict(self, obj, type=None):
+        if isinstance(obj, Base):
+            return obj.as_json_dict()
+        elif isinstance(obj, dict):
+            ret = {to_camel_case(k): self.serialize_as_dict(v) for k, v in obj.items()}
+            if type is not None:
+                ret['type'] = type
+            return ret
+        elif isinstance(obj, list):
+            return [self.serialize_as_dict(elem) for elem in obj]
+        else:
+            if PY3:
+                self.assertIsInstance(obj, (str, bool, Number))
+            else:
+                self.assertIsInstance(obj, (basestring, bool, Number))  # noqa
+            return obj
+
+    class ConstError(TypeError):
+        pass
+
+    def __setattr__(self, name, value):
+        if name in SerializeTestCase.__dict__:
+            raise self.ConstError("Can't rebind const (%s)" % name)
+        self.__dict__[name] = value
diff --git a/tests/models/test_actions.py b/tests/models/test_actions.py
new file mode 100644
index 0000000..6305488
--- /dev/null
+++ b/tests/models/test_actions.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the "License"); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+from linebot.models import (
+    PostbackAction,
+    MessageAction,
+    CameraRollAction,
+    CameraAction,
+    URIAction,
+    LocationAction,
+    DatetimePickerAction,
+    AltUri,
+)
+from tests.models.serialize_test_case import SerializeTestCase
+
+
+class TestActions(SerializeTestCase):
+    def test_postback(self):
+        arg = {
+            'label': 'Buy',
+            'data': 'action=buy&id=1',
+            'display_text': 'buy'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.POSTBACK),
+            PostbackAction(**arg).as_json_dict()
+        )
+
+    def test_message(self):
+        arg = {
+            'label': 'Yes',
+            'text': 'yes'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.MESSAGE),
+            MessageAction(**arg).as_json_dict()
+        )
+
+    def test_camera(self):
+        arg = {
+            'label': 'camera'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.CAMERA),
+            CameraAction(**arg).as_json_dict()
+        )
+
+    def test_camera_roll(self):
+        arg = {
+            'label': 'camera roll'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.CAMERA_ROLL),
+            CameraRollAction(**arg).as_json_dict()
+        )
+
+    def test_datetime_picker(self):
+        arg = {
+            'label': 'Select date',
+            'data': 'storeId=12345',
+            'mode': 'datetime',
+            'initial': '2017-12-25t00:00',
+            'max': '2018-01-24t23:59',
+            'min': '2017-12-25t00:00'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.DATETIMEPICKER),
+            DatetimePickerAction(**arg).as_json_dict()
+        )
+
+    def test_uri(self):
+        arg = {
+            'label': 'View detail',
+            'uri': 'https://example.com',
+            'alt_uri': AltUri(desktop='https://example.com')
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.URI),
+            URIAction(**arg).as_json_dict()
+        )
+
+    def test_location(self):
+        arg = {
+            'label': 'Location'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.LOCATION),
+            LocationAction(**arg).as_json_dict()
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/models/test_flex_message.py b/tests/models/test_flex_message.py
new file mode 100644
index 0000000..ac01108
--- /dev/null
+++ b/tests/models/test_flex_message.py
@@ -0,0 +1,225 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the 'License'); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+from linebot.models import (
+    FlexSendMessage,
+    BlockStyle,
+    BubbleStyle,
+    BubbleContainer,
+    CarouselContainer,
+    BoxComponent,
+    TextComponent,
+    SeparatorComponent,
+    ImageComponent,
+    ButtonComponent,
+    FillerComponent,
+    IconComponent,
+    SpacerComponent,
+    URIAction,
+)
+from tests.models.serialize_test_case import SerializeTestCase
+
+
+class TestFlexMessage(SerializeTestCase):
+    def test_flex_message(self):
+        arg = {
+            'alt_text': 'this is a flex message',
+            'contents':
+                BubbleContainer(
+                    body=BoxComponent(
+                        layout='vertical',
+                        contents=[
+                            TextComponent(text='hello'),
+                            TextComponent(text='world')
+                        ]
+                    )
+                )
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.FLEX),
+            FlexSendMessage(**arg).as_json_dict()
+        )
+
+    def test_bubble_container(self):
+        arg = {
+            'header':
+                BoxComponent(layout='vertical',
+                             contents=[TextComponent(text='Header text')]),
+            'hero':
+                ImageComponent(uri='https://example.com/flex/images/image.jpg'),
+            'body':
+                BoxComponent(layout='vertical',
+                             contents=[TextComponent(text='Body text')]),
+            'footer':
+                BoxComponent(layout='vertical',
+                             contents=[TextComponent(text='Footer text')]),
+            'styles':
+                BubbleStyle(
+                    header=BlockStyle(background_color='#00ffff'),
+                    hero=BlockStyle(background_color='#00ffff',
+                                    separator=True),
+                    footer=BlockStyle(background_color='#00ffff',
+                                      separator=True,
+                                      separator_color='#00ffff')
+                )
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.BUBBLE),
+            BubbleContainer(**arg).as_json_dict()
+        )
+
+    def test_bubble_style(self):
+        arg = {
+            'header':
+                BlockStyle(background_color='#00ffff'),
+            'hero':
+                BlockStyle(background_color='#00ffff',
+                           separator=True),
+            'footer':
+                BlockStyle(background_color='#00ffff',
+                           separator=True,
+                           separator_color='#00ffff')
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            BubbleStyle(**arg).as_json_dict()
+        )
+
+    def test_block_style(self):
+        arg = {
+            'background_color': '#00ffff',
+            'separator': True,
+            'separator_color': '#000000'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            BlockStyle(**arg).as_json_dict()
+        )
+
+    def test_carousel_container(self):
+        arg = {
+            'contents': [
+                BubbleContainer(
+                    body=BoxComponent(
+                        layout='vertical',
+                        contents=[TextComponent(text='Hey')]
+                    )
+                ),
+                BubbleContainer(
+                    body=BoxComponent(
+                        layout='vertical',
+                        contents=[TextComponent(text='Foo')]
+                    )
+                ),
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.CAROUSEL),
+            CarouselContainer(**arg).as_json_dict()
+        )
+
+    def test_box_component(self):
+        arg = {
+            'layout': 'vertical',
+            'contents': [
+                ImageComponent(url='https://example.com/flex/images/image.jpg'),
+                SeparatorComponent(),
+                TextComponent(text='Text in the box'),
+            ]
+        }
+
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.BOX),
+            BoxComponent(**arg).as_json_dict()
+        )
+
+    def test_button_component(self):
+        arg = {
+            'action':
+                URIAction(label='Tap me',
+                          uri='https://example.com'),
+            'style': 'primary',
+            'color': '#0000ff'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.BUTTON),
+            ButtonComponent(**arg).as_json_dict()
+        )
+
+    def test_filler_component(self):
+        arg = {}
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.FILLER),
+            FillerComponent(**arg).as_json_dict()
+        )
+
+    def test_icon_component(self):
+        arg = {
+            'url': 'https://example.com/icon/png/caution.png',
+            'size': 'lg',
+            'aspect_ratio': '1.91:1'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.ICON),
+            IconComponent(**arg).as_json_dict()
+        )
+
+    def test_image_component(self):
+        arg = {
+            'url': 'https://example.com/flex/images/image.jpg',
+            'size': 'full',
+            'aspect_ratio': '1.91:1'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.IMAGE),
+            ImageComponent(**arg).as_json_dict()
+        )
+
+    def test_separator_component(self):
+        arg = {
+            'color': '#000000'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.SEPARATOR),
+            SeparatorComponent(**arg).as_json_dict()
+        )
+
+    def test_spacer_component(self):
+        arg = {
+            'size': 'md'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.SPACER),
+            SpacerComponent(**arg).as_json_dict()
+        )
+
+    def test_text_component(self):
+        arg = {
+            'text': 'Hello, World!',
+            'size': 'xl',
+            'weight': 'bold',
+            'color': '#0000ff'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.TEXT),
+            TextComponent(**arg).as_json_dict()
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/models/test_imagemap.py b/tests/models/test_imagemap.py
new file mode 100644
index 0000000..814df0e
--- /dev/null
+++ b/tests/models/test_imagemap.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the 'License'); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+from linebot.models import (
+    ImagemapArea,
+    ImagemapSendMessage,
+    URIImagemapAction,
+    MessageImagemapAction,
+    Video, BaseSize, ExternalLink,
+)
+from tests.models.serialize_test_case import SerializeTestCase
+
+
+class TestImageMap(SerializeTestCase):
+    def test_image_map(self):
+        arg = {
+            'base_url': 'https://example.com/bot/images/rm001',
+            'alt_text': 'This is an imagemap',
+            'base_size': BaseSize(width=1040, height=1040),
+            'video':
+                Video(
+                    original_content_url='https://example.com/video.mp4',
+                    preview_image_url='https://example.com/video_preview.jpg',
+                    area=ImagemapArea(x=0, y=0, width=1040, height=585),
+                    external_link=ExternalLink(label='See more',
+                                               link_uri='https://example.com/see_more.html')
+                ),
+            'actions': [
+                URIImagemapAction(
+                    link_uri='https://example.com/',
+                    area=ImagemapArea(x=0, y=585, width=520, height=454)
+                ),
+                MessageImagemapAction(
+                    text='Hey',
+                    area=ImagemapArea(x=0, y=58, width=52, height=40)
+                )
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.IMAGEMAP),
+            ImagemapSendMessage(**arg).as_json_dict()
+        )
+
+    def test_video(self):
+        arg = {
+            'original_content_url': 'https://example.com/video.mp4',
+            'preview_image_url': 'https://example.com/video_preview.jpg',
+            'area': ImagemapArea(x=0, y=0, width=1040, height=585),
+            'external_link': ExternalLink(label='See more',
+                                          link_uri='https://example.com/see_more.html')
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            Video(**arg).as_json_dict()
+        )
+
+    def test_message_actions(self):
+        arg = {
+            'text': 'Hey',
+            'area': ImagemapArea(x=0, y=58, width=52, height=40)
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.MESSAGE),
+            MessageImagemapAction(**arg).as_json_dict()
+        )
+
+    def test_uri_actions(self):
+        arg = {
+            'link_uri': 'https://example.com/',
+            'area': ImagemapArea(x=0, y=585, width=520, height=454)
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, self.URI),
+            URIImagemapAction(**arg).as_json_dict()
+        )
+
+    def test_base_size(self):
+        arg = {
+            'width': 1040,
+            'height': 1040
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            BaseSize(**arg).as_json_dict()
+        )
+
+    def test_external_link(self):
+        arg = {
+            'label': 'Hey link',
+            'link_uri': 'https://example.com/see_more.html'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            ExternalLink(**arg).as_json_dict()
+        )
+
+    def test_imagemap_area(self):
+        arg = {
+            'x': 111,
+            'y': 33,
+            'width': 1040,
+            'height': 1040
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            ImagemapArea(**arg).as_json_dict()
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/models/test_send_messages.py b/tests/models/test_send_messages.py
new file mode 100644
index 0000000..0e89911
--- /dev/null
+++ b/tests/models/test_send_messages.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the 'License'); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+from linebot.models import (
+    TextSendMessage,
+    StickerSendMessage,
+    VideoSendMessage,
+    AudioSendMessage,
+    LocationSendMessage,
+    ImageSendMessage,
+    QuickReplyButton,
+    QuickReply,
+    CameraRollAction,
+    CameraAction
+)
+from tests.models.serialize_test_case import SerializeTestCase
+
+
+class TestSendMessages(SerializeTestCase):
+    def test_text_message(self):
+        arg = {
+            'text': 'Hello, world'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.TEXT),
+            TextSendMessage(**arg).as_json_dict()
+        )
+
+    def test_sticker_message(self):
+        arg = {
+            'package_id': '1',
+            'sticker_id': '1'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.STICKER),
+            StickerSendMessage(**arg).as_json_dict()
+        )
+
+    def test_image_message(self):
+        arg = {
+            'original_content_url': 'https://example.com/original.jpg',
+            'preview_image_url': 'https://example.com/preview.jpg'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.IMAGE),
+            ImageSendMessage(**arg).as_json_dict()
+        )
+
+    def test_video_message(self):
+        arg = {
+            'type': 'video',
+            'original_content_url': 'https://example.com/original.mp4',
+            'preview_image_url': 'https://example.com/preview.jpg'
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.VIDEO),
+            VideoSendMessage(**arg).as_json_dict()
+        )
+
+    def test_audio_message(self):
+        arg = {
+            'original_content_url': 'https://example.com/original.m4a',
+            'duration': 60000
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.AUDIO),
+            AudioSendMessage(**arg).as_json_dict()
+        )
+
+    def test_location_message(self):
+        arg = {
+            'title': 'my location',
+            'address': '〒150-0002 東京都渋谷区渋谷２丁目２１−１',
+            'latitude': 35.65910807942215,
+            'longitude': 139.70372892916203
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.LOCATION),
+            LocationSendMessage(**arg).as_json_dict()
+        )
+
+    def test_quick_reply_button(self):
+        arg = {
+            'action': CameraRollAction(label='Send photo')
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.ACTION),
+            QuickReplyButton(**arg).as_json_dict()
+        )
+
+    def test_quick_reply(self):
+        arg = {
+            'items': [
+                QuickReplyButton(action=CameraRollAction(label='Send photo')),
+                QuickReplyButton(action=CameraAction(label='Open camera')),
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            QuickReply(**arg).as_json_dict()
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/models/test_template.py b/tests/models/test_template.py
new file mode 100644
index 0000000..0e029d3
--- /dev/null
+++ b/tests/models/test_template.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+
+#  Licensed under the Apache License, Version 2.0 (the 'License'); you may
+#  not use this file except in compliance with the License. You may obtain
+#  a copy of the License at
+#
+#       https://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations
+#  under the License.
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+
+from linebot.models import (
+    TemplateSendMessage,
+    CarouselTemplate,
+    CarouselColumn,
+    ImageCarouselTemplate,
+    ImageCarouselColumn,
+    ConfirmTemplate,
+    ButtonsTemplate,
+    PostbackAction,
+    URIAction,
+    MessageAction,
+)
+from tests.models.serialize_test_case import SerializeTestCase
+
+
+class TestTemplate(SerializeTestCase):
+    def test_template(self):
+        arg = {
+            'type': 'template',
+            'alt_text': 'This is a buttons template',
+            'template':
+                ButtonsTemplate(
+                    thumbnail_image_url='https=//example.com/bot/images/image.jpg',
+                    image_aspect_ratio='rectangle',
+                    image_size='cover',
+                    image_background_color='#FFFFFF',
+                    title='Menu',
+                    text='Please select',
+                    default_action=URIAction(label='View detail',
+                                             uri='http://example.com/page/123'),
+                    actions=[
+                        PostbackAction(label='Buy', data='action=buy&itemid=155'),
+                        URIAction(label='View detail', uri='http://example.com/page/155'),
+                    ]
+                )
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.TEMPLATE),
+            TemplateSendMessage(**arg).as_json_dict()
+        )
+
+    def test_button_template(self):
+        arg = {
+            'type': 'buttons',
+            'thumbnail_image_url': 'https=//example.com/bot/images/image.jpg',
+            'image_aspect_ratio': 'rectangle',
+            'image_size': 'cover',
+            'image_background_color': '#FFFFFF',
+            'title': 'Menu',
+            'text': 'Please select',
+            'default_action': URIAction(label='View detail',
+                                        uri='http://example.com/page/123'),
+            'actions': [
+                PostbackAction(label='Buy', data='action=buy&itemid=155'),
+                URIAction(label='View detail', uri='http://example.com/page/155'),
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.BUTTONS),
+            ButtonsTemplate(**arg).as_json_dict()
+        )
+
+    def test_confirm_template(self):
+        arg = {
+            'type': 'confirm',
+            'text': 'Are you sure?',
+            'actions': [
+                MessageAction(label='Yes', text='yes...'),
+                MessageAction(label='No', text='no...'),
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.CONFIRM),
+            ConfirmTemplate(**arg).as_json_dict()
+        )
+
+    def test_carousel_template(self):
+        arg = {
+            'image_aspect_ratio': 'rectangle',
+            'image_size': 'cover',
+            'columns': [
+                CarouselColumn(
+                    thumbnail_image_url='https://example.com/bot/images/item1.jpg',
+                    image_background_color='#FFFFFF',
+                    title='this is menu',
+                    text='description',
+                    default_action=URIAction(label='View detail',
+                                             uri='http://example.com/page/123'),
+                    actions=[
+                        PostbackAction(label='Buy', data='action=buy&itemid=155'),
+                        URIAction(label='View detail', uri='http://example.com/page/155'),
+                    ]
+                ),
+                CarouselColumn(
+                    thumbnail_image_url='https://example.com/bot/images/item2.jpg',
+                    image_background_color='#000000',
+                    title='this is menu',
+                    text='description',
+                    default_action=URIAction(label='View detail',
+                                             uri='http://example.com/page/222'),
+                    actions=[
+                        PostbackAction(label='Buy', data='action=buy&itemid=555'),
+                        URIAction(label='View detail', uri='http://example.com/page/555'),
+                    ]
+                )
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.CAROUSEL),
+            CarouselTemplate(**arg).as_json_dict()
+        )
+
+    def test_carousel_column(self):
+        arg = {
+            'thumbnail_image_url': 'https://example.com/bot/images/item1.jpg',
+            'image_background_color': '#FFFFFF',
+            'title': 'this is menu',
+            'text': 'description',
+            'default_action': URIAction(label='View detail',
+                                        uri='http://example.com/page/123'),
+            'actions': [
+                PostbackAction(label='Buy', data='action=buy&itemid=155'),
+                URIAction(label='View detail', uri='http://example.com/page/155'),
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            CarouselColumn(**arg).as_json_dict()
+        )
+
+    def test_image_carousel_template(self):
+        arg = {
+            'columns': [
+                ImageCarouselColumn(
+                    image_url='https://example.com/bot/images/item1.jpg',
+                    action=PostbackAction(label='Buy', data='action=buy&itemid=555')
+                ),
+                ImageCarouselColumn(
+                    image_url='https://example.com/bot/images/item2.jpg',
+                    action=MessageAction(label='Yes', text='yes')
+                ),
+            ]
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg, type=self.IMAGE_CAROUSEL),
+            ImageCarouselTemplate(**arg).as_json_dict()
+        )
+
+    def test_image_carousel_column(self):
+        arg = {
+            'image_url': 'https://example.com/bot/images/item1.jpg',
+            'action': PostbackAction(label='Buy', data='action=buy&itemid=555')
+        }
+        self.assertEqual(
+            self.serialize_as_dict(arg),
+            ImageCarouselColumn(**arg).as_json_dict()
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()