diff --git a/example/tests/test_parsers.py b/example/tests/test_parsers.py
deleted file mode 100644
index b83f70a7..00000000
--- a/example/tests/test_parsers.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import json
-from io import BytesIO
-
-from django.test import TestCase, override_settings
-from django.urls import path, reverse
-from rest_framework import status, views
-from rest_framework.exceptions import ParseError
-from rest_framework.response import Response
-from rest_framework.test import APITestCase
-
-from rest_framework_json_api import serializers
-from rest_framework_json_api.parsers import JSONParser
-from rest_framework_json_api.renderers import JSONRenderer
-
-
-class TestJSONParser(TestCase):
-    def setUp(self):
-        class MockRequest(object):
-            def __init__(self):
-                self.method = "GET"
-
-        request = MockRequest()
-
-        self.parser_context = {"request": request, "kwargs": {}, "view": "BlogViewSet"}
-
-        data = {
-            "data": {
-                "id": 123,
-                "type": "Blog",
-                "attributes": {"json-value": {"JsonKey": "JsonValue"}},
-            },
-            "meta": {"random_key": "random_value"},
-        }
-
-        self.string = json.dumps(data)
-
-    @override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize")
-    def test_parse_include_metadata_format_field_names(self):
-        parser = JSONParser()
-
-        stream = BytesIO(self.string.encode("utf-8"))
-        data = parser.parse(stream, None, self.parser_context)
-
-        self.assertEqual(data["_meta"], {"random_key": "random_value"})
-        self.assertEqual(data["json_value"], {"JsonKey": "JsonValue"})
-
-    def test_parse_invalid_data(self):
-        parser = JSONParser()
-
-        string = json.dumps([])
-        stream = BytesIO(string.encode("utf-8"))
-
-        with self.assertRaises(ParseError):
-            parser.parse(stream, None, self.parser_context)
-
-    def test_parse_invalid_data_key(self):
-        parser = JSONParser()
-
-        string = json.dumps(
-            {
-                "data": [
-                    {
-                        "id": 123,
-                        "type": "Blog",
-                        "attributes": {"json-value": {"JsonKey": "JsonValue"}},
-                    }
-                ]
-            }
-        )
-        stream = BytesIO(string.encode("utf-8"))
-
-        with self.assertRaises(ParseError):
-            parser.parse(stream, None, self.parser_context)
-
-
-class DummyDTO:
-    def __init__(self, response_dict):
-        for k, v in response_dict.items():
-            setattr(self, k, v)
-
-    @property
-    def pk(self):
-        return self.id if hasattr(self, "id") else None
-
-
-class DummySerializer(serializers.Serializer):
-    body = serializers.CharField()
-    id = serializers.IntegerField()
-
-
-class DummyAPIView(views.APIView):
-    parser_classes = [JSONParser]
-    renderer_classes = [JSONRenderer]
-    resource_name = "dummy"
-
-    def patch(self, request, *args, **kwargs):
-        serializer = DummySerializer(DummyDTO(request.data))
-        return Response(status=status.HTTP_200_OK, data=serializer.data)
-
-
-urlpatterns = [
-    path("repeater", DummyAPIView.as_view(), name="repeater"),
-]
-
-
-class TestParserOnAPIView(APITestCase):
-    def setUp(self):
-        class MockRequest(object):
-            def __init__(self):
-                self.method = "PATCH"
-
-        request = MockRequest()
-        # To be honest view string isn't resolved into actual view
-        self.parser_context = {"request": request, "kwargs": {}, "view": "DummyAPIView"}
-
-        self.data = {
-            "data": {
-                "id": 123,
-                "type": "strs",
-                "attributes": {"body": "hello"},
-            }
-        }
-
-        self.string = json.dumps(self.data)
-
-    def test_patch_doesnt_raise_attribute_error(self):
-        parser = JSONParser()
-
-        stream = BytesIO(self.string.encode("utf-8"))
-
-        data = parser.parse(stream, None, self.parser_context)
-
-        assert data["id"] == 123
-        assert data["body"] == "hello"
-
-    @override_settings(ROOT_URLCONF=__name__)
-    def test_patch_request(self):
-        url = reverse("repeater")
-        data = self.data
-        data["data"]["type"] = "dummy"
-        response = self.client.patch(url, data=data)
-        data = response.json()
-
-        assert data["data"]["id"] == str(123)
-        assert data["data"]["attributes"]["body"] == "hello"
diff --git a/tests/conftest.py b/tests/conftest.py
index 22be93a1..ebdf5348 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,5 @@
 import pytest
+from rest_framework.test import APIClient
 
 from tests.models import (
     BasicModel,
@@ -51,3 +52,8 @@ def many_to_many_targets(db):
         ManyToManyTarget.objects.create(name="Target1"),
         ManyToManyTarget.objects.create(name="Target2"),
     ]
+
+
+@pytest.fixture
+def client():
+    return APIClient()
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
new file mode 100644
index 00000000..907d1eb6
--- /dev/null
+++ b/tests/test_parsers.py
@@ -0,0 +1,130 @@
+import json
+from io import BytesIO
+
+import pytest
+from rest_framework.exceptions import ParseError
+
+from rest_framework_json_api.parsers import JSONParser
+from rest_framework_json_api.utils import format_value
+from tests.views import BasicModelViewSet
+
+
+class TestJSONParser:
+    @pytest.fixture
+    def parser(self):
+        return JSONParser()
+
+    @pytest.fixture
+    def parse(self, parser, parser_context):
+        def parse_wrapper(data):
+            stream = BytesIO(json.dumps(data).encode("utf-8"))
+            return parser.parse(stream, None, parser_context)
+
+        return parse_wrapper
+
+    @pytest.fixture
+    def parser_context(self, rf):
+        return {"request": rf.post("/"), "kwargs": {}, "view": BasicModelViewSet()}
+
+    @pytest.mark.parametrize(
+        "format_field_names",
+        [
+            None,
+            "dasherize",
+            "camelize",
+            "capitalize",
+            "underscore",
+        ],
+    )
+    def test_parse_formats_field_names(
+        self,
+        settings,
+        format_field_names,
+        parse,
+    ):
+        settings.JSON_API_FORMAT_FIELD_NAMES = format_field_names
+
+        data = {
+            "data": {
+                "id": "123",
+                "type": "BasicModel",
+                "attributes": {
+                    format_value("test_attribute", format_field_names): "test-value"
+                },
+                "relationships": {
+                    format_value("test_relationship", format_field_names): {
+                        "data": {"type": "TestRelationship", "id": "123"}
+                    }
+                },
+            }
+        }
+
+        result = parse(data)
+        assert result == {
+            "id": "123",
+            "test_attribute": "test-value",
+            "test_relationship": {"id": "123", "type": "TestRelationship"},
+        }
+
+    def test_parse_extracts_meta(self, parse):
+        data = {
+            "data": {
+                "type": "BasicModel",
+            },
+            "meta": {"random_key": "random_value"},
+        }
+
+        result = parse(data)
+        assert result["_meta"] == data["meta"]
+
+    def test_parse_preserves_json_value_field_names(self, settings, parse):
+        settings.JSON_API_FORMAT_FIELD_NAMES = "dasherize"
+
+        data = {
+            "data": {
+                "type": "BasicModel",
+                "attributes": {"json-value": {"JsonKey": "JsonValue"}},
+            },
+        }
+
+        result = parse(data)
+        assert result["json_value"] == {"JsonKey": "JsonValue"}
+
+    def test_parse_raises_error_on_empty_data(self, parse):
+        data = []
+
+        with pytest.raises(ParseError) as excinfo:
+            parse(data)
+        assert "Received document does not contain primary data" == str(excinfo.value)
+
+    def test_parse_fails_on_list_of_objects(self, parse):
+        data = {
+            "data": [
+                {
+                    "type": "BasicModel",
+                    "attributes": {"json-value": {"JsonKey": "JsonValue"}},
+                }
+            ],
+        }
+
+        with pytest.raises(ParseError) as excinfo:
+            parse(data)
+
+        assert "Received data is not a valid JSONAPI Resource Identifier Object" == str(
+            excinfo.value
+        )
+
+    def test_parse_fails_when_id_is_missing_on_patch(self, rf, parse, parser_context):
+        parser_context["request"] = rf.patch("/")
+        data = {
+            "data": {
+                "type": "BasicModel",
+            },
+        }
+
+        with pytest.raises(ParseError) as excinfo:
+            parse(data)
+
+        assert "The resource identifier object must contain an 'id' member" == str(
+            excinfo.value
+        )
diff --git a/tests/test_relations.py b/tests/test_relations.py
index d66c602f..1baafdd0 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -11,13 +11,14 @@
     SerializerMethodHyperlinkedRelatedField,
 )
 from rest_framework_json_api.utils import format_link_segment
-from rest_framework_json_api.views import ModelViewSet, RelationshipView
+from rest_framework_json_api.views import RelationshipView
 from tests.models import BasicModel
 from tests.serializers import (
     ForeignKeySourceSerializer,
     ManyToManySourceReadOnlySerializer,
     ManyToManySourceSerializer,
 )
+from tests.views import BasicModelViewSet
 
 
 @pytest.mark.django_db
@@ -266,11 +267,6 @@ def test_get_links(
 # Routing setup
 
 
-class BasicModelViewSet(ModelViewSet):
-    class Meta:
-        model = BasicModel
-
-
 class BasicModelRelationshipView(RelationshipView):
     queryset = BasicModel.objects
 
diff --git a/tests/test_views.py b/tests/test_views.py
index 8241a10e..e419ea0f 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,49 +1,104 @@
 import pytest
+from django.urls import path, reverse
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.views import APIView
 
-from rest_framework_json_api import serializers, views
+from rest_framework_json_api import serializers
+from rest_framework_json_api.parsers import JSONParser
 from rest_framework_json_api.relations import ResourceRelatedField
+from rest_framework_json_api.renderers import JSONRenderer
 from rest_framework_json_api.utils import format_value
+from rest_framework_json_api.views import ModelViewSet
+from tests.models import BasicModel
 
-from .models import BasicModel
 
-related_model_field_name = "related_field_model"
+class TestModelViewSet:
+    @pytest.mark.parametrize(
+        "format_links",
+        [
+            None,
+            "dasherize",
+            "camelize",
+            "capitalize",
+            "underscore",
+        ],
+    )
+    def test_get_related_field_name_handles_formatted_link_segments(
+        self, format_links, rf
+    ):
+        # use field name which actually gets formatted
+        related_model_field_name = "related_field_model"
 
+        class RelatedFieldNameSerializer(serializers.ModelSerializer):
+            related_model_field = ResourceRelatedField(queryset=BasicModel.objects)
 
-@pytest.mark.parametrize(
-    "format_links",
-    [
-        None,
-        "dasherize",
-        "camelize",
-        "capitalize",
-        "underscore",
-    ],
-)
-def test_get_related_field_name_handles_formatted_link_segments(format_links, rf):
-    url_segment = format_value(related_model_field_name, format_links)
+            def __init__(self, *args, **kwargs):
+                self.related_model_field.field_name = related_model_field_name
+                super().__init(*args, **kwargs)
 
-    request = rf.get(f"/basic_models/1/{url_segment}")
+            class Meta:
+                model = BasicModel
 
-    view = BasicModelFakeViewSet()
-    view.setup(request, related_field=url_segment)
+        class RelatedFieldNameView(ModelViewSet):
+            serializer_class = RelatedFieldNameSerializer
 
-    assert view.get_related_field_name() == related_model_field_name
+        url_segment = format_value(related_model_field_name, format_links)
 
+        request = rf.get(f"/basic_models/1/{url_segment}")
 
-class BasicModelSerializer(serializers.ModelSerializer):
-    related_model_field = ResourceRelatedField(queryset=BasicModel.objects)
+        view = RelatedFieldNameView()
+        view.setup(request, related_field=url_segment)
 
-    def __init__(self, *args, **kwargs):
-        # Intentionally setting field_name property to something that matches no format
-        self.related_model_field.field_name = related_model_field_name
-        super(BasicModelSerializer, self).__init(*args, **kwargs)
+        assert view.get_related_field_name() == related_model_field_name
 
-    class Meta:
-        model = BasicModel
 
+class TestAPIView:
+    @pytest.mark.urls(__name__)
+    def test_patch(self, client):
+        data = {
+            "data": {
+                "id": 123,
+                "type": "custom",
+                "attributes": {"body": "hello"},
+            }
+        }
 
-class BasicModelFakeViewSet(views.ModelViewSet):
-    serializer_class = BasicModelSerializer
+        url = reverse("custom")
 
-    def retrieve(self, request, *args, **kwargs):
-        pass
+        response = client.patch(url, data=data)
+        result = response.json()
+
+        assert result["data"]["id"] == str(123)
+        assert result["data"]["type"] == "custom"
+        assert result["data"]["attributes"]["body"] == "hello"
+
+
+class CustomModel:
+    def __init__(self, response_dict):
+        for k, v in response_dict.items():
+            setattr(self, k, v)
+
+    @property
+    def pk(self):
+        return self.id if hasattr(self, "id") else None
+
+
+class CustomModelSerializer(serializers.Serializer):
+    body = serializers.CharField()
+    id = serializers.IntegerField()
+
+
+class CustomAPIView(APIView):
+    parser_classes = [JSONParser]
+    renderer_classes = [JSONRenderer]
+    resource_name = "custom"
+
+    def patch(self, request, *args, **kwargs):
+        serializer = CustomModelSerializer(CustomModel(request.data))
+        return Response(status=status.HTTP_200_OK, data=serializer.data)
+
+
+urlpatterns = [
+    path("custom", CustomAPIView.as_view(), name="custom"),
+]
diff --git a/tests/views.py b/tests/views.py
new file mode 100644
index 00000000..e7046a52
--- /dev/null
+++ b/tests/views.py
@@ -0,0 +1,10 @@
+from rest_framework_json_api.views import ModelViewSet
+from tests.models import BasicModel
+from tests.serializers import BasicModelSerializer
+
+
+class BasicModelViewSet(ModelViewSet):
+    serializer_class = BasicModelSerializer
+
+    class Meta:
+        model = BasicModel