Repository: ai4os/DEEPaaS (language: python)

Issue:
Ability to define "basePath"
### Description

Sometimes, especially when behind the reverse proxy, it might be useful to have "basePath" for the API not as `/` but as something else, e.g `/api`. It would be nice, if a user could (re-)define this "basePath"

In the case of OAS3, in swagger.yaml this would correspond to:

```
servers:
  - url: /api
```

Ref: https://swagger.io/docs/specification/api-host-and-base-path/




Discussion / hints:

Fix patch:
diff --git a/deepaas/api/__init__.py b/deepaas/api/__init__.py
index 9a12e939..e1b99e4a 100644
--- a/deepaas/api/__init__.py
+++ b/deepaas/api/__init__.py
@@ -14,6 +14,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import pathlib
 
 from aiohttp import web
 import aiohttp_apispec
@@ -75,10 +76,22 @@ async def get_app(
     model.register_v2_models(APP)
 
     v2app = v2.get_app(enable_train=enable_train, enable_predict=enable_predict)
-    APP.add_subapp("/v2", v2app)
+    if base_path:
+        path = str(pathlib.Path(base_path) / "v2")
+    else:
+        path = "/v2"
+    APP.add_subapp(path, v2app)
     versions.register_version("stable", v2.get_version)
 
-    APP.add_routes(versions.routes)
+    if base_path:
+        # Get versions.routes, and transform them to have the base_path, as we cannot
+        # directly modify the routes already created and stored in the RouteTableDef
+        for route in versions.routes:
+            APP.router.add_route(
+                route.method, str(pathlib.Path(base_path + route.path)), route.handler
+            )
+    else:
+        APP.add_routes(versions.routes)
 
     LOG.info("Serving loaded V2 models: %s", list(model.V2_MODELS.keys()))
 
@@ -88,6 +101,9 @@ async def get_app(
             await m.warm()
 
     if swagger:
+        doc = str(pathlib.Path(base_path + doc))
+        swagger = str(pathlib.Path(base_path + "/swagger.json"))
+
         # init docs with all parameters, usual for ApiSpec
         aiohttp_apispec.setup_aiohttp_apispec(
             app=APP,
@@ -101,7 +117,7 @@ async def get_app(
             },
             basePath=base_path,
             version=deepaas.extract_version(),
-            url="/swagger.json",
+            url=swagger,
             swagger_path=doc if enable_doc else None,
             prefix=prefix,
             static_path=static_path,
diff --git a/deepaas/api/versions.py b/deepaas/api/versions.py
index d6241533..48df6c8d 100644
--- a/deepaas/api/versions.py
+++ b/deepaas/api/versions.py
@@ -25,7 +25,7 @@
 routes = web.RouteTableDef()
 
 
-@routes.view("/")
+@routes.view("/", name="versions")
 class Versions(web.View):
     versions = {}
 
diff --git a/deepaas/cmd/cli.py b/deepaas/cmd/cli.py
index e15cfbc7..c037b7d2 100644
--- a/deepaas/cmd/cli.py
+++ b/deepaas/cmd/cli.py
@@ -227,7 +227,6 @@ def _add_methods(subparsers):
     cfg.StrOpt(
         "deepaas_method_output",
         help="Define an output file, if needed",
-        deprecated_name="deepaas_model_output",
     ),
     cfg.BoolOpt(
         "deepaas_with_multiprocessing",
diff --git a/deepaas/cmd/execute.py b/deepaas/cmd/execute.py
deleted file mode 100644
index c1cdc8b6..00000000
--- a/deepaas/cmd/execute.py
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env python -*- coding: utf-8 -*-
-
-# Copyright 2020 Spanish National Research Council (CSIC)
-#
-# 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
-#
-#      http://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.
-
-import mimetypes
-import os
-import shutil
-import sys
-import warnings
-
-from oslo_config import cfg
-from oslo_log import log
-
-from deepaas.model import loading
-from deepaas.model.v2.wrapper import UploadedFile
-
-cli_opts = [
-    cfg.StrOpt(
-        "input-file",
-        short="i",
-        help="""
-Set local input file to predict.
-""",
-    ),
-    cfg.StrOpt(
-        "content-type",
-        default="application/json",
-        short="ct",
-        help="""
-Especify the content type of the output file. The selected
-option must be available in the model used.
-(by default application/json).
-""",
-    ),
-    cfg.StrOpt(
-        "output",
-        short="o",
-        help="""
-Save the result to a local file.
-""",
-    ),
-    cfg.BoolOpt(
-        "url",
-        short="u",
-        default=False,
-        help="""
-Run as input file an URL.
-If this option is set to True, we can use the URL
-of an image as an input file.
-""",
-    ),
-]
-
-CONF = cfg.CONF
-CONF.register_cli_opts(cli_opts)
-
-LOG = log.getLogger(__name__)
-
-# Loading the model installed
-
-
-def get_model_name():
-    model_name = CONF.model_name
-    models = loading.get_available_models("v2")
-    if model_name:
-        model_obj = models.get(model_name)
-        if model_obj is None:
-            sys.stderr.write(
-                "ERROR: The model {} is not available.\n".format(model_name)
-            )
-            sys.exit(1)
-        return model_name, model_obj
-    elif len(models) == 1:
-        return models.popitem()
-    else:
-        sys.stderr.write(
-            "ERROR: There are several models available ({}) "
-            "you have to choose one.\n".format(list(models.keys()))
-        )
-        sys.exit(1)
-
-
-def prediction(input_file, file_type, content_type):
-    model_name, model_obj = get_model_name()
-    predict_data = model_obj.predict_data
-    predict_url = model_obj.predict_url
-
-    if file_type is True:
-        input_data = {"urls": [input_file], "accept": content_type}
-        output_pred = predict_url(input_data)
-    else:
-        content_type_in, file_encoding = mimetypes.guess_type(input_file)
-        file = UploadedFile(
-            name=input_file, filename=input_file, content_type=content_type_in
-        )
-        input_data = {"files": [file], "accept": content_type}
-        output_pred = predict_data(input_data)
-
-    return output_pred
-
-
-def main():
-    msg = (
-        "\033[0;31;40mWARNING: This command is deprectated, please use "
-        "deepaas-cli instead. \n"
-        "WARNING: This command will be removed in the next major release. \033[0m"
-    )
-    warnings.warn(msg, DeprecationWarning, stacklevel=2)
-    sys.stderr.write(msg + "\n")
-
-    cfg.CONF(sys.argv[1:])
-    input_file = CONF.input_file
-    content_type = CONF.content_type
-    file_type = CONF.url
-    output = CONF.output
-
-    # Checking required argument
-    if input_file is None:
-        sys.stderr.write("ERROR: Option input_file is required.\n")
-        sys.exit(1)
-
-    if output is None:
-        sys.stderr.write("ERROR: Option output is required.\n")
-        sys.exit(1)
-
-    output_pred = prediction(input_file, file_type, content_type)
-    extension = mimetypes.guess_extension(content_type)
-    if extension is None or output_pred is None:
-        sys.stderr.write("ERROR: Content type {} not valid.\n".format(content_type))
-        sys.exit(1)
-    if extension == ".json":
-        name_image = os.path.splitext(os.path.basename(input_file))[0]
-        out_file_name = "out_" + name_image
-        f = open(out_file_name + ".json", "w+")
-        f.write(repr(output_pred) + "\n")
-        f.close()
-        if not os.path.exists(output):  # Create path if does not exist
-            os.makedirs(output)
-        dir_name = output + f.name
-        shutil.move(f.name, os.path.join(output, f.name))
-    else:
-        output_path_image = output_pred.name
-        dir_name = output + os.path.basename(output_path_image)
-        if not os.path.exists(output):  # Create path if does not exist
-            os.makedirs(output)
-        shutil.copy(output_path_image, output)
-
-    print("Output saved at {}".format(dir_name))
-
-
-if __name__ == "__main__":
-    main()
diff --git a/deepaas/cmd/run.py b/deepaas/cmd/run.py
index e1a9d0d3..5f616860 100644
--- a/deepaas/cmd/run.py
+++ b/deepaas/cmd/run.py
@@ -15,6 +15,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import pathlib
 import sys
 
 from aiohttp import web
@@ -92,7 +93,17 @@ def main():
     config.config_and_logging(sys.argv)
     log = oslo_log.getLogger("deepaas")
 
-    base = "http://{}:{}".format(CONF.listen_ip, CONF.listen_port)
+    base_path = CONF.base_path
+
+    if base_path and not base_path.startswith("/"):
+        print("Base path should start with a '/'.", file=sys.stderr)
+        sys.exit(1)
+    elif base_path:
+        base_path = str(pathlib.Path(base_path))
+    else:
+        base_path = ""
+
+    base = "http://{}:{}{}".format(CONF.listen_ip, CONF.listen_port, base_path)
     spec = "{}/swagger.json".format(base)
     docs = "{}/api".format(base)
     v2 = "{}/v2".format(base)
diff --git a/deepaas/exceptions.py b/deepaas/exceptions.py
index 2f15fb3b..93bf5507 100644
--- a/deepaas/exceptions.py
+++ b/deepaas/exceptions.py
@@ -17,3 +17,7 @@
 
 class ModuleNotFoundError(Exception):
     """Module not found error."""
+
+
+class NoModelsAvailable(Exception):
+    """No models are available in the system."""
diff --git a/deepaas/model/v2/__init__.py b/deepaas/model/v2/__init__.py
index bb82969d..e0940be0 100644
--- a/deepaas/model/v2/__init__.py
+++ b/deepaas/model/v2/__init__.py
@@ -21,7 +21,6 @@
 from deepaas import config
 from deepaas import exceptions
 from deepaas.model import loading
-from deepaas.model.v2 import test
 from deepaas.model.v2 import wrapper
 
 LOG = log.getLogger(__name__)
@@ -54,14 +53,8 @@ def register_models(app):
         LOG.error("Model not found: %s", CONF.model_name)
         raise
     except Exception as e:
-        # We do not raise here, as we have not yet removed the deprecated loading of the
-        # test module... but we should remove it as soon as the code below is deprecated
         LOG.warning("Error loading models: %s", e)
-        warnings.warn(
-            "Error loading models, using test model. This will be deprecated soon.",
-            DeprecationWarning,
-            stacklevel=2,
-        )
+        raise e
 
     if MODELS:
         if len(MODELS) > 1:
@@ -74,16 +67,6 @@ def register_models(app):
         return
 
     if not MODELS:
-        # Raise deprecation warning
-        warn_msg = (
-            "Using the built-in test model is deprecated, if you are testing the "
-            "API, please use the demo_app instead. "
-            "Check https://github.com/deephdc/demo_app for more information.",
-        )
-        warnings.warn(warn_msg, DeprecationWarning, stacklevel=2)
-        LOG.info("No models found in V2, loading test model")
-        LOG.warning(warn_msg)
-        MODELS["deepaas-test"] = wrapper.ModelWrapper(
-            "deepaas-test", test.TestModel(), app
-        )
+        LOG.error("No models found in V2, loading test model")
+        raise exceptions.NoModelsAvailable()
     MODELS_LOADED = True
diff --git a/doc/source/api.rst b/doc/source/api.rst
index 6009ef05..82cd9b5a 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -21,10 +21,3 @@ Model wrapper
 .. automodule:: deepaas.model.v2
    :members:
    :inherited-members:
-
-V2 test model
-#############
-
-.. automodule:: deepaas.model.v2.test
-   :members:
-   :inherited-members:
diff --git a/doc/source/cli/deepaas-predict.rst b/doc/source/cli/deepaas-predict.rst
deleted file mode 100644
index 469395d3..00000000
--- a/doc/source/cli/deepaas-predict.rst
+++ /dev/null
@@ -1,63 +0,0 @@
-================
-deepaas-predict
-================
-
-Synopsis
-========
-
-:program:`deepaas-predict` [options]
-
-Description
-===========
-
-:program:`deepaas-predict` It is a command that allows you to obtain,
-    through the command line, the prediction of a file or the url of
-    a file, of the models that are loaded through the ``deepaas.v2.models``
-    entrypoint API.
-
-Options
-=======
-
-.. option:: --input-file INPUT_FILE, -i INPUT_FILE
-
-   Set local input file to predict. This option is required.
-
-.. option:: --content-type CONTENT_TYPE, -c CONTENT_TYPE
-
-   Especify the content type of the output file. The selected
-   option must be available in the model used.
-   (by default application/json).
-
-.. option:: --model-name MODEL_NAME
-
-    Specify the model to be used. If not specified, DEEPaaS will serve all the models
-    that are available. If specified, DEEPaaS will serve only the specified model. You
-    can also use the DEEPAAS_V2_MODEL environment variable.
-
-    WARNING: Serving multiple models is deprecated and will be removed in the future,
-    therefore it is strongly suggested that you specify the model you want to or that
-    you ensure that only one model is available.
-
-.. option:: --output OUTPUT_DIR, -o OUTPUT_DIR
-
-   Save the result to a local file. This option is required.
-
-.. option:: --url, -u
-
-   If set to true, the input file is the url o a file to predict.
-
-Files
-=====
-
-None
-
-See Also
-========
-
-Documentation: `DEEPaaS API <https://docs.deep-hybrid-datacloud.eu/projects/deepaas/>`_
-
-Reporting Bugs
-==============
-
-Bugs are managed at `GitHub <https://github.com/indigo-dc/deepaas>`_
-
diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst
index 772d3c29..44130f8a 100644
--- a/doc/source/cli/index.rst
+++ b/doc/source/cli/index.rst
@@ -6,5 +6,4 @@ Available commands
    :maxdepth: 1
 
    deepaas-cli
-   deepaas-predict
    deepaas-run
diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst
index 1bf9027c..c9812536 100644
--- a/doc/source/user/index.rst
+++ b/doc/source/user/index.rst
@@ -6,4 +6,3 @@ User and developer documentation
 
    quickstart
    v2-api
-   predict
diff --git a/doc/source/user/predict.rst b/doc/source/user/predict.rst
deleted file mode 100644
index dc7c9db7..00000000
--- a/doc/source/user/predict.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-.. _predict:
-
-DEEPaaS API as a command line action
-====================================
-
-Support for execution from the command line for DEEPaaS. In your Dockerfile,
-you must ensure that you execute `` deepaas-predict`` in one of the following ways:
-
-Basic form of execution.
-
-.. code-block:: console
-
-   (...)
-   CMD ["sh", "-c", "deepaas-predict -i INPUT_FILE -o OUTPUT_DIR"]
-
-Execution specifying a content type option.
-
-.. code-block:: console
-
-   (...)
-   CMD ["sh", "-c", "deepaas-predict -i INPUT_FILE -c CONTENT_TYPE -o OUTPUT_DIR"]
-
-Execution specifying an url as an input file.
-
-.. code-block:: console
-
-   (...)
-   CMD ["sh", "-c", "deepaas-predict -i INPUT_FILE --url -o OUTPUT_DIR"]
-
-Execution specifying the name of a model.
-
-.. code-block:: console
-
-   (...)
-   CMD ["sh", "-c", "deepaas-predict -i INPUT_FILE --model-name MODEL_NAME -o OUTPUT_DIR"]
-
diff --git a/pyproject.toml b/pyproject.toml
index fb6fbf07..85d33a5f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,7 +40,6 @@ classifiers = [
 
 [tool.poetry.scripts]
 deepaas-run = "deepaas.cmd.run:main"
-deepaas-predict = "deepaas.cmd.execute:main"
 deepaas-cli = "deepaas.cmd.cli:main"
 
 [tool.poetry.plugins] # Optional super table 
@@ -68,11 +67,10 @@ tox = "^4.15.1"
 pytest = "^8.2.2"
 pytest-aiohttp = "^1.0.5"
 pytest-cov = "^5.0.0"
-fixtures = "^4.1.0"
 reno = "^4.1.0"
 mock = "^5.1.0"
-testtools = "^2.7.1"
 setuptools = "^70.0.0"
+pytest-asyncio = "^0.23.7"
 
 
 [tool.poetry.group.test-flake8.dependencies]
@@ -102,6 +100,14 @@ twine = "^5.1.0"
 
 [tool.poetry.group.test-pip-missing-reqs.dependencies]
 pip-check-reqs = "^2.5.3"
+poetry-plugin-export = "^1.8.0"
+
+
+[tool.poetry.group.test-docs.dependencies]
+sphinx = "^7.3.7"
+sphinx-rtd-theme = "^2.0.0"
+recommonmark = "^0.7.1"
+reno = "^4.1.0"
 
 [build-system]
 requires = ["poetry-core"]
diff --git a/releasenotes/notes/deprecate-deepaas-predict-f92939d443cb1904.yaml b/releasenotes/notes/deprecate-deepaas-predict-f92939d443cb1904.yaml
index 7560650e..512edf4e 100644
--- a/releasenotes/notes/deprecate-deepaas-predict-f92939d443cb1904.yaml
+++ b/releasenotes/notes/deprecate-deepaas-predict-f92939d443cb1904.yaml
@@ -1,4 +1,4 @@
 ---
-deprecations:
+upgrade: 
   - |
-    The command ``deepaas-predict`` is now deprecated, please use ``deepaas-cli`` instead.
+    The command ``deepaas-predict`` has been removed, please use ``deepaas-cli`` instead.
diff --git a/releasenotes/notes/deprecation-removals-1f43c9301633ac76.yaml b/releasenotes/notes/deprecation-removals-1f43c9301633ac76.yaml
new file mode 100644
index 00000000..96347bf9
--- /dev/null
+++ b/releasenotes/notes/deprecation-removals-1f43c9301633ac76.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Removed deprecated loading of "deepaas-test" model. If you wish to develop
+    or debug your model, use the "demo-app" instead: https://github.com/ai4os-hub/ai4os-demo-app
diff --git a/tox.ini b/tox.ini
index 4b13acde..96355085 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,9 @@ python = python3.11
 package = deepaas
 
 [pytest]
-addopts = -p no:warnings
+addopts = -p no:warnings -rs
+asyncio_mode = auto
+testpaths = tests
 
 [testenv]
 usedevelop = True
@@ -40,8 +42,8 @@ setenv =
    OS_STDOUT_CAPTURE=1
    OS_STDERR_CAPTURE=1
    OS_TEST_TIMEOUT=160
-commands_pre =                                                                             
-    poetry install --no-root --sync --with test,test-{envname} 
+commands_pre =
+    poetry install --no-root --sync --with test,test-{envname}
 commands =
     find . -type f -name "*.pyc" -delete
     poetry run pytest {posargs}
@@ -61,15 +63,15 @@ basepython = python3.12
 commands_pre =
     poetry install --no-root --sync --with test
 
-[testenv:cov]                                                                              
-basepython = python3.10                                                                    
-commands_pre =                                                                          
-    poetry install --no-root --sync --with test                                         
-commands =                                                                              
-    find . -type f -name "*.pyc" -delete                                                
-    poetry run pytest {posargs} --cov={[base]package} \                                 
-           --cov-report term \                                                          
-           --cov-report=xml       
+[testenv:cov]
+basepython = python3.11
+commands_pre =
+    poetry install --no-root --sync --with test
+commands =
+    find . -type f -name "*.pyc" -delete
+    poetry run pytest {posargs} --cov={[base]package} \
+           --cov-report term \
+           --cov-report=html
 
 [flake8]
 # Black default line length is 88
@@ -101,34 +103,35 @@ commands =
 
 [testenv:black]
 basepython = {[base]python}
-commands = 
+commands =
     poetry run black --check --diff {[base]package}
 
 [testenv:bandit]
 basepython = {[base]python}
-commands = 
+commands =
     poetry run bandit -r {[base]package} -x tests -s B110,B410
 
 [testenv:pypi]
 basepython = {[base]python}
-commands = 
+commands =
      poetry publish --build --dry-run
 
 [testenv:genconfig]
 basepython = {[base]python}
-commands_pre =                                                                             
+commands_pre =
     poetry install --no-root --sync --with test
     pip install -U .
-commands = 
+commands =
     poetry run oslo-config-generator --config-file=etc/{[base]package}-config-generator.conf
 
 [testenv:docs]
 basepython = {[base]python}
-deps =
-    -r {toxinidir}/doc/requirements.txt
+commands_pre =
+    poetry install --no-root --sync --with test --with test-docs
+    pip install -U .
 commands =
     rm -rf doc/build
-    sphinx-build -W --keep-going -b html -j auto doc/source doc/build/html
+    poetry run sphinx-build -W --keep-going -b html -j auto doc/source doc/build/html
 
 [testenv:mypy]
 description = Static type checks
@@ -138,8 +141,8 @@ commands =
 
 [testenv:pip-missing-reqs]
 basepython = {[base]python}
-commands =                                                                              
-    mkdir -p tmp                                                                        
-    poetry export -o tmp/requirements.txt                                               
-    poetry run pip-missing-reqs --requirements-file tmp/requirements.txt --ignore-file=data/* --ignore-file={[base]package}/tests/* {[base]package} 
-    rm tmp/requirements.txt    
+commands =
+    mkdir -p tmp
+    poetry export -o tmp/requirements.txt
+    poetry run pip-missing-reqs --requirements-file tmp/requirements.txt --ignore-file=data/* --ignore-file={[base]package}/tests/* {[base]package}
+    rm tmp/requirements.txt


Test patch:
diff --git a/deepaas/tests/base.py b/deepaas/tests/base.py
deleted file mode 100644
index 2ca0c1bc..00000000
--- a/deepaas/tests/base.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2018 Spanish National Research Council (CSIC)
-#
-# 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
-#
-#      http://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.
-
-import os
-
-import aiohttp.test_utils
-from aiohttp import web
-import fixtures
-from oslo_config import cfg
-from oslo_config import fixture as config_fixture
-from oslo_log import log as logging
-import testtools
-
-CONF = cfg.CONF
-logging.register_options(CONF)
-
-
-_TRUE_VALUES = ("True", "true", "1", "yes")
-
-
-class TestCase(testtools.TestCase, aiohttp.test_utils.AioHTTPTestCase):
-    """Base unit test class."""
-
-    async def get_application(self):
-        app = web.Application(debug=True)
-        app.middlewares.append(web.normalize_path_middleware())
-
-        return app
-
-    def setUp(self):
-        """Run before each test method to initialize test environment."""
-
-        super(TestCase, self).setUp()
-
-        test_timeout = os.environ.get("TESTR_TEST_TIMEOUT", 0)
-        try:
-            test_timeout = int(test_timeout)
-        except ValueError:
-            # If timeout value is invalid do not set a timeout.
-            test_timeout = 0
-        if test_timeout > 0:
-            self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
-
-        self.useFixture(config_fixture.Config(CONF))
-
-        if os.environ.get("TESTR_STDOUT_CAPTURE") in _TRUE_VALUES:
-            stdout = self.useFixture(fixtures.StringStream("stdout")).stream
-            self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout))
-        if os.environ.get("TESTR_STDERR_CAPTURE") in _TRUE_VALUES:
-            stderr = self.useFixture(fixtures.StringStream("stderr")).stream
-            self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr))
-
-    async def asyncSetUp(self):
-        """Run before each test method to initialize test environment."""
-
-        await super(TestCase, self).asyncSetUp()
-        self.assertEqual(self.app.debug, True)
-
-    def flags(self, **kw):
-        """Override flag variables for a test."""
-        group = kw.pop("group", None)
-        for k, v in kw.items():
-            CONF.set_override(k, v, group)
-
-    def runTest(self):  # noqa(N802)
-        """This is due to https://github.com/pytest-dev/pytest/issues/12263"""
-        pass
diff --git a/deepaas/tests/fake_responses.py b/deepaas/tests/fake_responses.py
index 7512655e..28337ff6 100644
--- a/deepaas/tests/fake_responses.py
+++ b/deepaas/tests/fake_responses.py
@@ -77,4 +77,7 @@
     "labels": [{"label": "foo", "probability": 1.0}],
 }
 
-deepaas_test_train = {"status": "running", "uuid": "2ad53089edfb4521af081c45df16bed5"}
+deepaas_test_train = {
+    "status": "running",
+    "args": {"sleep": 0},
+}
diff --git a/deepaas/model/v2/test.py b/deepaas/tests/fake_v2_model.py
similarity index 98%
rename from deepaas/model/v2/test.py
rename to deepaas/tests/fake_v2_model.py
index c045dced..d4121f9a 100644
--- a/deepaas/model/v2/test.py
+++ b/deepaas/tests/fake_v2_model.py
@@ -35,7 +35,6 @@ class TestModel(base.BaseModel):
 
     name = "deepaas-test"
 
-    # FIXME(aloga): document this
     schema = {
         "date": fields.Date(),
         "labels": fields.List(
@@ -135,7 +134,7 @@ def get_predict_args(self):
             "accept": fields.Str(
                 metadata={
                     "description": (
-                        "Media type(s) that is/are acceptable for the " "response."
+                        "Media type(s) that is/are acceptable for the response."
                     ),
                     "location": "headers",
                 },
@@ -151,7 +150,7 @@ def get_train_args(self):
                 required=True,
                 metadata={
                     "descripton": (
-                        "This is a integer parameter, and it is " "a required one."
+                        "This is a integer parameter, and it is a required one."
                     ),
                 },
             ),
diff --git a/deepaas/tests/test_api.py b/deepaas/tests/test_api.py
new file mode 100644
index 00000000..46abd619
--- /dev/null
+++ b/deepaas/tests/test_api.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2018 Spanish National Research Council (CSIC)
+#
+# 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
+#
+#      http://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.
+
+import io
+
+import pytest
+
+import deepaas
+from deepaas import api
+from deepaas.model.v2 import wrapper as v2_wrapper
+from deepaas.tests import fake_responses
+from deepaas.tests import fake_v2_model
+
+
+class TestApiV2:
+    @pytest.fixture
+    @staticmethod
+    def wrapped_model():
+        return v2_wrapper.ModelWrapper("deepaas-test", fake_v2_model.TestModel(), None)
+
+    @pytest.fixture
+    @staticmethod
+    async def client(application, aiohttp_client):
+        c = await aiohttp_client(application)
+        return c
+
+    @pytest.fixture
+    @staticmethod
+    async def application(monkeypatch):
+        w = v2_wrapper.ModelWrapper("deepaas-test", fake_v2_model.TestModel(), None)
+
+        monkeypatch.setattr(api, "APP", None)
+        monkeypatch.setattr(deepaas.model, "V2_MODELS", {"deepaas-test": w})
+        monkeypatch.setattr(deepaas.model, "register_v2_models", lambda x: None)
+
+        app = await api.get_app(enable_doc=False, base_path="/custom")
+
+        return app
+
+    async def test_not_found(self, client):
+        ret = await client.get("/api")
+        assert 404 == ret.status
+
+    async def test_predict_data(self, client):
+        f = io.BytesIO(b"foo")
+        ret = await client.post(
+            "/custom/v2/models/deepaas-test/predict/",
+            data={"data": (f, "foo.txt"), "parameter": 1},
+        )
+        assert 200 == ret.status
+
+        json = await ret.json()
+
+        assert "data" in json
+        del json["data"]
+
+        assert fake_responses.deepaas_test_predict == json
+
+    async def test_train(self, client):
+        ret = await client.post(
+            "/custom/v2/models/deepaas-test/train/", data={"sleep": 0}
+        )
+        assert 200 == ret.status
+
+        json = await ret.json()
+
+        assert "date" in json
+        json.pop("date")
+
+        assert "uuid" in json
+        del json["uuid"]
+
+        assert fake_responses.deepaas_test_train == json
+
+    async def test_get_metadata(self, client):
+        meta = fake_responses.models_meta
+
+        ret = await client.get("/custom/v2/models/")
+        assert ret.status in [200, 201]
+
+        ret_meta = await ret.json()
+        ret_meta["models"][0]["links"][0]["href"] = "/v2/models/deepaas-test"
+
+        assert meta == ret_meta
+
+        ret = await client.get("/custom/v2/models/deepaas-test/")
+        assert ret.status in [200, 201]
+
+        ret_meta = await ret.json()
+        ret_meta["links"][0]["href"] = "/v2/models/deepaas-test"
+
+        assert meta["models"][0] == ret_meta
diff --git a/deepaas/tests/test_cmd.py b/deepaas/tests/test_cmd.py
index 10029284..871ec0e4 100644
--- a/deepaas/tests/test_cmd.py
+++ b/deepaas/tests/test_cmd.py
@@ -14,101 +14,64 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-import os
-import shutil
 import sys
-import urllib.request
 
 import mock
+from oslo_config import cfg
+import pytest
 
-from deepaas.cmd import execute
 from deepaas.cmd import run
-from deepaas.tests import base
+import deepaas.config
 
+CONF = cfg.CONF
 
-class TestRun(base.TestCase):
-    @mock.patch("deepaas.cmd._shutdown.handle_signals")
-    @mock.patch("aiohttp.web.run_app")
-    @mock.patch("deepaas.api.get_app")
-    def test_run(self, m_get_app, m_run_app, m_handle_signals):
-        m = mock.MagicMock()
-        m_get_app.return_value = m
-        with mock.patch.object(sys, "argv", ["deepaas-run"]):
-            run.main()
-        m_get_app.assert_called_once()
-        m_run_app.assert_called_with(
-            mock.ANY,
-            host="127.0.0.1",
-            port=5000,
-        )
-        m_handle_signals.assert_called_once()
 
-    @mock.patch("deepaas.cmd._shutdown.handle_signals")
-    @mock.patch("aiohttp.web.run_app")
-    @mock.patch("deepaas.api.get_app")
-    def test_run_custom_ip_port(self, m_get_app, m_run_app, m_handle_signals):
-        m = mock.MagicMock()
-        m_get_app.return_value = m
-        ip = "1.1.1.1"
-        port = 1234
-        self.flags(listen_ip=ip)
-        self.flags(listen_port=port)
-        with mock.patch.object(sys, "argv", ["deepaas-run"]):
-            run.main()
-        m_get_app.assert_called_once()
-        m_run_app.assert_called_with(
-            mock.ANY,
-            host=ip,
-            port=port,
-        )
-        m_handle_signals.assert_called_once()
+@mock.patch("deepaas.cmd._shutdown.handle_signals")
+@mock.patch("aiohttp.web.run_app")
+@mock.patch("deepaas.api.get_app")
+async def test_run(m_get_app, m_run_app, m_handle_signals):
+    m = mock.MagicMock()
+    m_get_app.return_value = m
+    with mock.patch.object(sys, "argv", ["deepaas-run"]):
+        run.main()
+    m_get_app.assert_called_once()
+    m_run_app.assert_called_with(
+        mock.ANY,
+        host="127.0.0.1",
+        port=5000,
+    )
+    m_handle_signals.assert_called_once()
 
 
-class TestExecute(base.TestCase):
-    @mock.patch("deepaas.cmd.execute.prediction")
-    def test_execute_data(self, m_out_pred):
-        in_file = "file"
-        out_file = "deepaas/tests/out_test/"
-        self.flags(input_file=in_file)
-        self.flags(output=out_file)
-        m_out_pred.return_value = [{"value1": {"pred": 1}, "value2": {"pred": 0.9}}]
-        with mock.patch.object(sys, "argv", ["deepaas-predict"]):
-            execute.main()
+@pytest.fixture
+def cfg_fixture():
+    def set_flag(flag, value):
+        CONF.set_override(flag, value)
 
-    @mock.patch("deepaas.cmd.execute.prediction")
-    def test_execute_url(self, m_out_pred):
-        in_file = "https://xxxxxxxxxx"
-        out_file = "deepaas/tests/out_test/"
-        self.flags(input_file=in_file)
-        self.flags(output=out_file)
-        self.flags(url=True)
-        m_out_pred.return_value = [{"value1": {"pred": 1}, "value2": {"pred": 0.9}}]
-        with mock.patch.object(sys, "argv", ["deepaas-predict"]):
-            execute.main()
+    return set_flag
 
-    @mock.patch("deepaas.cmd.execute.prediction")
-    def test_execute_ct(self, m_out_pred):
-        in_file = "file"
-        out_file = "deepaas/tests/out_test/"
-        cont_type = "application/zip"
-        self.flags(input_file=in_file)
-        self.flags(output=out_file)
-        self.flags(content_type=cont_type)
-        output_dir = "deepaas/tests/tmp_dir"
-        if os.path.exists(output_dir):
-            shutil.rmtree(output_dir)
-        os.mkdir(output_dir)
-        out_json = [{"value1": {"pred": 1}, "value2": {"pred": 0.9}}]
-        f_json = open(output_dir + "output" + ".json", "w+")
-        f_json.write(repr(out_json) + "\n")
-        f_json.close
-        url = "https://storage.googleapis.com/" "tfjs-models/assets/posenet/frisbee.jpg"
-        urllib.request.urlretrieve(url, "example_image.jpg")
-        shutil.move(f_json.name, output_dir)
-        shutil.move("example_image.jpg", output_dir)
-        f = shutil.make_archive(base_name=output_dir, format="zip", root_dir=output_dir)
-        m_out_pred.return_value = open(f, "rb")
-        with mock.patch.object(sys, "argv", ["deepaas-predict"]):
-            execute.main()
-        shutil.rmtree(output_dir)
-        os.remove(output_dir + ".zip")
+
+@mock.patch("deepaas.cmd._shutdown.handle_signals")
+@mock.patch("aiohttp.web.run_app")
+@mock.patch("deepaas.api.get_app")
+async def test_run_custom_ip_port(
+    m_get_app, m_run_app, m_handle_signals, cfg_fixture, monkeypatch
+):
+
+    monkeypatch.setattr(deepaas.config, "config_and_logging", lambda x: None)
+    m = mock.MagicMock()
+    m_get_app.return_value = m
+    ip = "1.1.1.1"
+    port = 1234
+
+    cfg_fixture("listen_ip", ip)
+    cfg_fixture("listen_port", port)
+    with mock.patch.object(sys, "argv", ["deepaas-run"]):
+        run.main()
+    m_get_app.assert_called_once()
+    m_run_app.assert_called_with(
+        mock.ANY,
+        host=ip,
+        port=port,
+    )
+    m_handle_signals.assert_called_once()
diff --git a/deepaas/tests/test_v2_api.py b/deepaas/tests/test_v2_api.py
index b97a1948..62f8e5c6 100644
--- a/deepaas/tests/test_v2_api.py
+++ b/deepaas/tests/test_v2_api.py
@@ -20,6 +20,7 @@
 from aiohttp import web
 from oslo_config import cfg
 from oslo_log import log as logging
+import pytest
 
 import deepaas
 from deepaas.api import v2
@@ -27,8 +28,9 @@
 from deepaas.api.v2 import responses
 import deepaas.model
 import deepaas.model.v2
-from deepaas.tests import base
+from deepaas.model.v2 import wrapper as v2_wrapper
 from deepaas.tests import fake_responses
+from deepaas.tests import fake_v2_model
 
 CONF = cfg.CONF
 logging.register_options(CONF)
@@ -42,270 +44,163 @@ class Fake(object):
     assert response is responses.Prediction
 
 
-class TestApiV2NoTrain(base.TestCase):
-    async def get_application(self):
-        app = web.Application(debug=True)
-        app.middlewares.append(web.normalize_path_middleware())
+@pytest.fixture(autouse=True)
+def cfg_fixture():
+    def set_flag(flag, value):
+        CONF.set_override(flag, value)
 
-        deepaas.model.v2.register_models(app)
+    return set_flag
 
-        v2app = v2.get_app(enable_train=False)
-        app.add_subapp("/v2", v2app)
-
-        return app
 
-    def setUp(self):
-        super(TestApiV2NoTrain, self).setUp()
+class BaseTestApiV2:
+    @pytest.fixture
+    @staticmethod
+    def wrapped_model():
+        return v2_wrapper.ModelWrapper("deepaas-test", fake_v2_model.TestModel(), None)
 
-        self.maxDiff = None
+    @pytest.fixture
+    @staticmethod
+    async def client(application, aiohttp_client):
+        c = await aiohttp_client(application)
+        return c
 
-        self.flags(debug=True)
+    async def test_get_metadata(self, client):
+        meta = fake_responses.models_meta
 
-    def assert_ok(self, response):
-        self.assertIn(response.status, [200, 201])
+        ret = await client.get("/v2/models/")
+        assert ret.status in [200, 201]
+        assert meta == await ret.json()
 
-    async def test_not_found(self):
-        ret = await self.client.post("/v2/models/deepaas-test/train")
-        self.assertEqual(404, ret.status)
+        ret = await client.get("/v2/models/deepaas-test/")
+        assert ret.status in [200, 201]
+        assert meta["models"][0] == await ret.json()
 
-    async def test_predict_data(self):
+    async def test_predict_data(self, client):
         f = io.BytesIO(b"foo")
-        ret = await self.client.post(
+        ret = await client.post(
             "/v2/models/deepaas-test/predict/",
             data={"data": (f, "foo.txt"), "parameter": 1},
         )
-        json = await ret.json()
-        self.assertEqual(200, ret.status)
-        self.assertDictEqual(fake_responses.deepaas_test_predict, json)
-
-    async def test_get_metadata(self):
-        meta = fake_responses.models_meta
-
-        ret = await self.client.get("/v2/models/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta, await ret.json())
-
-        ret = await self.client.get("/v2/models/deepaas-test/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta["models"][0], await ret.json())
-
-
-class TestApiV2NoPredict(base.TestCase):
-    async def get_application(self):
-        app = web.Application(debug=True)
-        app.middlewares.append(web.normalize_path_middleware())
-
-        deepaas.model.v2.register_models(app)
-
-        v2app = v2.get_app(enable_predict=False)
-        app.add_subapp("/v2", v2app)
-
-        return app
+        assert 200 == ret.status
 
-    def setUp(self):
-        super(TestApiV2NoPredict, self).setUp()
-
-        self.maxDiff = None
+        json = await ret.json()
 
-        self.flags(debug=True)
+        assert "data" in json
+        del json["data"]
 
-    def assert_ok(self, response):
-        self.assertIn(response.status, [200, 201])
+        assert fake_responses.deepaas_test_predict == json
 
-    async def test_not_found(self):
-        ret = await self.client.post("/v2/models/deepaas-test/predict/")
-        self.assertEqual(404, ret.status)
+    async def test_train(self, client):
+        ret = await client.post("/v2/models/deepaas-test/train/", data={"sleep": 0})
+        assert 200 == ret.status
 
-    async def test_train(self):
-        ret = await self.client.post(
-            "/v2/models/deepaas-test/train/", data={"sleep": 0}
-        )
-        self.assertEqual(200, ret.status)
         json = await ret.json()
-        json.pop("date")
-        self.assertDictEqual(fake_responses.deepaas_test_train, json)
 
-    async def test_get_metadata(self):
-        meta = fake_responses.models_meta
+        assert "date" in json
+        json.pop("date")
 
-        ret = await self.client.get("/v2/models/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta, await ret.json())
+        assert "uuid" in json
+        json.pop("uuid")
 
-        ret = await self.client.get("/v2/models/deepaas-test/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta["models"][0], await ret.json())
+        assert fake_responses.deepaas_test_train == json
 
 
-class TestApiV2NoDoc(base.TestCase):
-    async def get_application(self):
-        app = web.Application(debug=True)
+class TestApiV2NoTrain(BaseTestApiV2):
+    @pytest.fixture
+    @staticmethod
+    async def application(monkeypatch):
+        app = web.Application()
         app.middlewares.append(web.normalize_path_middleware())
 
-        deepaas.model.v2.register_models(app)
+        w = v2_wrapper.ModelWrapper("deepaas-test", fake_v2_model.TestModel(), app)
 
-        v2app = v2.get_app(enable_doc=False)
+        monkeypatch.setattr(deepaas.model, "V2_MODELS", {"deepaas-test": w})
+
+        v2app = v2.get_app(enable_train=False)
         app.add_subapp("/v2", v2app)
 
         return app
 
-    def setUp(self):
-        super(TestApiV2NoDoc, self).setUp()
-
-        self.maxDiff = None
-
-        self.flags(debug=True)
+    async def test_train(self, client):
+        ret = await client.post("/v2/models/deepaas-test/train")
+        assert 402 == ret.status
 
-    def assert_ok(self, response):
-        self.assertIn(response.status, [200, 201])
 
-    async def test_not_found(self):
-        ret = await self.client.get("/api")
-        self.assertEqual(404, ret.status)
-
-
-class TestApiV2CusomBasePath(base.TestCase):
-    async def get_application(self):
-        app = web.Application(debug=True)
+class TestApiV2NoPredict(BaseTestApiV2):
+    @pytest.fixture
+    @staticmethod
+    async def application(monkeypatch):
+        app = web.Application()
         app.middlewares.append(web.normalize_path_middleware())
 
-        deepaas.model.v2.register_models(app)
+        w = v2_wrapper.ModelWrapper("deepaas-test", fake_v2_model.TestModel(), app)
 
-        v2app = v2.get_app(base_path="/custom")
+        monkeypatch.setattr(deepaas.model, "V2_MODELS", {"deepaas-test": w})
+
+        v2app = v2.get_app(enable_predict=False)
         app.add_subapp("/v2", v2app)
 
         return app
 
-    def setUp(self):
-        super(TestApiV2CusomBasePath, self).setUp()
+    async def test_predict_data(self, client):
+        ret = await client.post("/v2/models/deepaas-test/predict/")
+        assert 402 == ret.status
 
-        self.maxDiff = None
 
-        self.flags(debug=True)
-
-    def assert_ok(self, response):
-        self.assertIn(response.status, [200, 201])
-
-    async def test_predict_data(self):
-        f = io.BytesIO(b"foo")
-        ret = await self.client.post(
-            "/custom/v2/models/deepaas-test/predict/",
-            data={"data": (f, "foo.txt"), "parameter": 1},
-        )
-        json = await ret.json()
-        self.assertEqual(200, ret.status)
-        self.assertDictEqual(fake_responses.deepaas_test_predict, json)
-
-    async def test_train(self):
-        ret = await self.client.post(
-            "/custom/v2/models/deepaas-test/train/", data={"sleep": 0}
-        )
-        self.assertEqual(200, ret.status)
-        json = await ret.json()
-        json.pop("date")
-        self.assertDictEqual(fake_responses.deepaas_test_train, json)
-
-    async def test_get_metadata(self):
-        meta = fake_responses.models_meta
-
-        ret = await self.client.get("/custom/v2/models/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta, await ret.json())
-
-        ret = await self.client.get("/custom/v2/models/deepaas-test/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta["models"][0], await ret.json())
-
-
-class TestApiV2(base.TestCase):
-    async def get_application(self):
-        app = web.Application(debug=True)
+class TestApiV2(BaseTestApiV2):
+    @pytest.fixture
+    @staticmethod
+    async def application(monkeypatch):
+        app = web.Application()
         app.middlewares.append(web.normalize_path_middleware())
 
-        deepaas.model.v2.register_models(app)
+        w = v2_wrapper.ModelWrapper("deepaas-test", fake_v2_model.TestModel(), app)
+
+        monkeypatch.setattr(deepaas.model, "V2_MODELS", {"deepaas-test": w})
 
         v2app = v2.get_app()
         app.add_subapp("/v2", v2app)
 
         return app
 
-    def setUp(self):
-        super(TestApiV2, self).setUp()
+    async def test_not_found(self, client):
+        ret = await client.get("/v2/models/%s" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-        self.maxDiff = None
+        ret = await client.put("/v2/models/%s" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-        self.flags(debug=True)
+        ret = await client.post("/v2/models/%s" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-    def assert_ok(self, response):
-        self.assertIn(response.status, [200, 201])
+        ret = await client.delete("/v2/models/%s" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-    async def test_not_found(self):
-        ret = await self.client.get("/v2/models/%s" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
+    async def test_model_not_found(self, client):
+        ret = await client.get("/v2/models/%s/train" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-        ret = await self.client.put("/v2/models/%s" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
+        ret = await client.put("/v2/models/%s/train" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-        ret = await self.client.post("/v2/models/%s" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
+        ret = await client.post("/v2/models/%s/predict" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-        ret = await self.client.delete("/v2/models/%s" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
+        ret = await client.get("/v2/models/%s" % uuid.uuid4().hex)
+        assert 404 == ret.status
 
-    async def test_model_not_found(self):
-        ret = await self.client.put("/v2/models/%s/train" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
-
-        ret = await self.client.post("/v2/models/%s/predict" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
-
-        ret = await self.client.get("/v2/models/%s" % uuid.uuid4().hex)
-        self.assertEqual(404, ret.status)
-
-    async def test_predict_no_parameters(self):
-        ret = await self.client.post("/v2/models/deepaas-test/predict/")
+    async def test_predict_no_parameters(self, client):
+        ret = await client.post("/v2/models/deepaas-test/predict/")
         json = await ret.json()
-        print(json)
-        self.assertDictEqual(
-            {
-                "parameter": ["Missing data for required field."],
-                "data": ["Missing data for required field."],
-            },
-            json,
-        )
-        self.assertEqual(422, ret.status)
-
-    async def test_predict_data(self):
-        f = io.BytesIO(b"foo")
-        ret = await self.client.post(
-            "/v2/models/deepaas-test/predict/",
-            data={"data": (f, "foo.txt"), "parameter": 1},
-        )
-        json = await ret.json()
-        self.assertEqual(200, ret.status)
-        self.assertDictEqual(fake_responses.deepaas_test_predict, json)
-
-    async def test_train(self):
-        ret = await self.client.post(
-            "/v2/models/deepaas-test/train/", data={"sleep": 0}
-        )
-        self.assertEqual(200, ret.status)
-        json = await ret.json()
-        json.pop("date")
-        self.assertDictEqual(fake_responses.deepaas_test_train, json)
-
-    async def test_get_metadata(self):
-        meta = fake_responses.models_meta
-
-        ret = await self.client.get("/v2/models/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta, await ret.json())
 
-        ret = await self.client.get("/v2/models/deepaas-test/")
-        self.assert_ok(ret)
-        self.assertDictEqual(meta["models"][0], await ret.json())
+        expected = {
+            "parameter": ["Missing data for required field."],
+            "data": ["Missing data for required field."],
+        }
+        assert 422 == ret.status
+        assert expected == json
 
-    async def test_bad_metods_metadata(self):
-        for i in (self.client.post, self.client.put, self.client.delete):
+    async def test_bad_metods_metadata(self, client):
+        for i in (client.post, client.put, client.delete):
             ret = await i("/v2/models/")
-            self.assertEqual(405, ret.status)
+            assert 405 == ret.status
diff --git a/deepaas/tests/test_v2_models.py b/deepaas/tests/test_v2_models.py
index c7ef44f3..94e957a3 100644
--- a/deepaas/tests/test_v2_models.py
+++ b/deepaas/tests/test_v2_models.py
@@ -17,216 +17,223 @@
 import itertools
 import uuid
 
-from aiohttp import test_utils
 from aiohttp import web
 import marshmallow
 from marshmallow import fields as m_fields
-import mock
+import pytest
 from webargs import fields
 
 import deepaas
+from deepaas import exceptions
 import deepaas.model.v2
 from deepaas.model.v2 import base as v2_base
-from deepaas.model.v2 import test as v2_test
 from deepaas.model.v2 import wrapper as v2_wrapper
-from deepaas.tests import base
+from deepaas.tests import fake_v2_model
 
 
-class TestV2Model(base.TestCase):
-    def setUp(self):
-        super(TestV2Model, self).setUp()
-
-        deepaas.model.v2.MODELS_LOADED = False
-        deepaas.model.v2.MODELS = {}
-
-    def test_abc(self):
-        self.assertRaises(TypeError, v2_base.BaseModel)
-
-    def test_not_implemented(self):
-        class Model(v2_base.BaseModel):
-            def get_metadata(self):
-                super(Model, self).get_metadata()
-
-            def predict(self, **kwargs):
-                super(Model, self).predict(**kwargs)
-
-            def get_predict_args(self):
-                super(Model, self).get_predict_args()
-
-            def train(self, **kwargs):
-                super(Model, self).train(**kwargs)
-
-            def warm(self, **kwargs):
-                super(Model, self).train(**kwargs)
-
-            def get_train_args(self):
-                super(Model, self).get_train_args()
-
-        m = Model()
-        self.assertRaises(NotImplementedError, m.get_metadata)
-        self.assertRaises(NotImplementedError, m.predict)
-        self.assertRaises(NotImplementedError, m.get_predict_args)
-        self.assertRaises(NotImplementedError, m.train)
-        self.assertRaises(NotImplementedError, m.get_train_args)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    async def test_bad_schema(self, m_clean):
-        class Model(object):
-            schema = []
-
-        self.assertRaises(
-            web.HTTPInternalServerError,
-            v2_wrapper.ModelWrapper,
-            "test",
-            Model(),
-            self.app,
-        )
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    async def test_validate_no_schema(self, m_clean):
-        class Model(object):
-            schema = None
-
-        wrapper = v2_wrapper.ModelWrapper("test", Model(), self.app)
-        self.assertRaises(web.HTTPInternalServerError, wrapper.validate_response, None)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    async def test_invalid_schema(self, m_clean):
-        class Model(object):
-            schema = object()
-
-        self.assertRaises(
-            web.HTTPInternalServerError,
-            v2_wrapper.ModelWrapper,
-            "test",
-            Model(),
-            self.app,
-        )
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    async def test_marshmallow_schema(self, m_clean):
-        class Schema(marshmallow.Schema):
-            foo = m_fields.Str()
-
-        class Model(object):
-            schema = Schema
-
-        wrapper = v2_wrapper.ModelWrapper("test", Model(), self.app)
-
-        self.assertTrue(wrapper.validate_response({"foo": "bar"}))
-        self.assertRaises(
-            web.HTTPInternalServerError, wrapper.validate_response, {"foo": 1.0}
-        )
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    async def test_dict_schema(self, m_clean):
-        class Model(object):
-            schema = {"foo": m_fields.Str()}
-
-        wrapper = v2_wrapper.ModelWrapper("test", Model(), self.app)
-
-        self.assertTrue(wrapper.validate_response({"foo": "bar"}))
-        self.assertRaises(
-            web.HTTPInternalServerError, wrapper.validate_response, {"foo": 1.0}
-        )
-
-    def test_dummy_model(self):
-        m = v2_test.TestModel()
-        pred = m.predict()
-        pred.pop("data")
-        self.assertDictEqual(
-            {"date": "2019-01-1", "labels": [{"label": "foo", "probability": 1.0}]},
-            pred,
-        )
-        self.assertIsNone(m.train())
-        meta = m.get_metadata()
-        self.assertIn("description", meta)
-        self.assertIn("id", meta)
-        self.assertIn("name", meta)
-        self.assertEqual("Alvaro Lopez Garcia", meta["author"])
-        pargs = m.get_predict_args()
-        targs = m.get_train_args()
-        for _arg, val in itertools.chain(pargs.items(), targs.items()):
-            self.assertIsInstance(val, fields.Field)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    @test_utils.unittest_run_loop
-    async def test_dummy_model_with_wrapper(self, m_clean):
-        w = v2_wrapper.ModelWrapper("foo", v2_test.TestModel(), self.app)
-        task = w.predict()
-        await task
-        ret = task.result()["output"]
-        self.assertDictEqual(
-            {"date": "2019-01-1", "labels": [{"label": "foo", "probability": 1.0}]}, ret
-        )
-        task = w.train(sleep=0)
-        await task
-        ret = task.result()
-        self.assertIsNone(ret["output"])
-        meta = w.get_metadata()
-        self.assertIn("description", meta)
-        self.assertIn("id", meta)
-        self.assertIn("name", meta)
-        pargs = w.get_predict_args()
-        targs = w.get_train_args()
-        for _arg, val in itertools.chain(pargs.items(), targs.items()):
-            self.assertIsInstance(val, fields.Field)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    @test_utils.unittest_run_loop
-    async def test_model_with_not_implemented_attributes_and_wrapper(self, m_clean):
-        w = v2_wrapper.ModelWrapper("foo", object(), self.app)
-
-        # NOTE(aloga): Cannot use assertRaises here directly, as testtools
-        # overrides this method, and their implementation makes impossible to
-        # use it a s context manager. Then, since we need to do async calls to
-        # the methods (they are coroutines) we cannot use assertRaises
-        # directly.
-        try:
-            await w.predict()
-        except Exception as e:
-            self.assertIsInstance(e, web.HTTPNotImplemented)
-
-        try:
-            await w.train()
-        except Exception as e:
-            self.assertIsInstance(e, web.HTTPNotImplemented)
-
-        meta = w.get_metadata()
-        self.assertIn("description", meta)
-        self.assertIn("id", meta)
-        self.assertIn("name", meta)
-        pargs = w.get_predict_args()
-        targs = w.get_train_args()
-        for _arg, val in itertools.chain(pargs.items(), targs.items()):
-            self.assertIsInstance(val, fields.Field)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    @mock.patch("deepaas.model.loading.get_available_models")
-    async def test_loading_ok(self, mock_loading, m_clean):
-        mock_loading.return_value = {uuid.uuid4().hex: "bar"}
-        deepaas.model.v2.register_models(self.app)
-        mock_loading.assert_called()
-        for m in deepaas.model.v2.MODELS.values():
-            self.assertIsInstance(m, v2_wrapper.ModelWrapper)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    @mock.patch("deepaas.model.loading.get_available_models")
-    async def test_loading_ok_singleton(self, mock_loading, m_clean):
-        mock_loading.return_value = {uuid.uuid4().hex: "bar"}
-        deepaas.model.v2.register_models(self.app)
-        deepaas.model.v2.register_models(self.app)
-        mock_loading.assert_called_once()
-        for m in deepaas.model.v2.MODELS.values():
-            self.assertIsInstance(m, v2_wrapper.ModelWrapper)
-
-    @mock.patch("deepaas.model.v2.wrapper.ModelWrapper._setup_cleanup")
-    @mock.patch("deepaas.model.loading.get_available_models")
-    async def test_loading_error(self, mock_loading, m_clean):
-        mock_loading.return_value = {}
-        deepaas.model.v2.register_models(self.app)
-        mock_loading.assert_called()
-        self.assertIn("deepaas-test", deepaas.model.v2.MODELS)
-        m = deepaas.model.v2.MODELS.pop("deepaas-test")
-        self.assertIsInstance(m, v2_wrapper.ModelWrapper)
-        self.assertIsInstance(m.model_obj, v2_test.TestModel)
+@pytest.fixture
+async def application():
+    app = web.Application()
+    app.middlewares.append(web.normalize_path_middleware())
+
+    return app
+
+
+@pytest.fixture
+async def mocks(monkeypatch):
+    monkeypatch.setattr(v2_wrapper.ModelWrapper, "_setup_cleanup", lambda x: None)
+    monkeypatch.setattr(
+        deepaas.model.loading,
+        "get_available_models",
+        lambda x: {uuid.uuid4().hex: "bar"},
+    )
+    monkeypatch.setattr(deepaas.model.v2, "MODELS_LOADED", False)
+    monkeypatch.setattr(deepaas.model.v2, "MODELS", {})
+
+
+def test_abc():
+    with pytest.raises(TypeError):
+        v2_base.BaseModel()
+
+
+def test_not_implemented():
+    class Model(v2_base.BaseModel):
+        def get_metadata(self):
+            super(Model, self).get_metadata()
+
+        def predict(self, **kwargs):
+            super(Model, self).predict(**kwargs)
+
+        def get_predict_args(self):
+            super(Model, self).get_predict_args()
+
+        def train(self, **kwargs):
+            super(Model, self).train(**kwargs)
+
+        def warm(self, **kwargs):
+            super(Model, self).train(**kwargs)
+
+        def get_train_args(self):
+            super(Model, self).get_train_args()
+
+    m = Model()
+
+    with pytest.raises(NotImplementedError):
+        m.get_metadata()
+    with pytest.raises(NotImplementedError):
+        m.predict()
+    with pytest.raises(NotImplementedError):
+        m.get_predict_args()
+    with pytest.raises(NotImplementedError):
+        m.train()
+    with pytest.raises(NotImplementedError):
+        m.get_train_args()
+
+
+async def test_bad_schema(application, mocks):
+    class Model(object):
+        schema = []
+
+    with pytest.raises(web.HTTPInternalServerError):
+        v2_wrapper.ModelWrapper("test", Model(), application)
+
+
+async def test_validate_no_schema(application, mocks):
+    class Model(object):
+        schema = None
+
+    with pytest.raises(web.HTTPInternalServerError):
+        wrapper = v2_wrapper.ModelWrapper("test", Model(), application)
+        wrapper.validate_response(None)
+
+
+async def test_invalid_schema(application, mocks):
+    class Model(object):
+        schema = object()
+
+    with pytest.raises(web.HTTPInternalServerError):
+        v2_wrapper.ModelWrapper("test", Model(), application)
+
+
+async def test_marshmallow_schema(application, mocks):
+    class Schema(marshmallow.Schema):
+        foo = m_fields.Str()
+
+    class Model(object):
+        schema = Schema
+
+    wrapper = v2_wrapper.ModelWrapper("test", Model(), application)
+
+    assert wrapper.validate_response({"foo": "bar"})
+    with pytest.raises(web.HTTPInternalServerError):
+        wrapper.validate_response({"foo": 1.0})
+
+
+async def test_dict_schema(application, mocks):
+    class Model(object):
+        schema = {"foo": m_fields.Str()}
+
+    wrapper = v2_wrapper.ModelWrapper("test", Model(), application)
+
+    assert wrapper.validate_response({"foo": "bar"})
+    with pytest.raises(web.HTTPInternalServerError):
+        wrapper.validate_response({"foo": 1.0})
+
+
+@pytest.fixture
+def model():
+    return fake_v2_model.TestModel()
+
+
+async def test_dummy_model(model, mocks):
+    pred = model.predict()
+    pred.pop("data")
+    assert pred == {
+        "date": "2019-01-1",
+        "labels": [{"label": "foo", "probability": 1.0}],
+    }
+
+    assert model.train() is None
+
+    meta = model.get_metadata()
+    assert "description" in meta
+    assert "id" in meta
+    assert "name" in meta
+    assert "Alvaro Lopez Garcia" == meta["author"]
+
+    pargs = model.get_predict_args()
+    targs = model.get_train_args()
+    for _arg, val in itertools.chain(pargs.items(), targs.items()):
+        assert isinstance(val, fields.Field)
+
+
+async def test_dummy_model_with_wrapper(application, model, mocks):
+    w = v2_wrapper.ModelWrapper("foo", model, application)
+    task = w.predict()
+    await task
+    ret = task.result()["output"]
+    del ret["data"]
+    assert ret == {
+        "date": "2019-01-1",
+        "labels": [{"label": "foo", "probability": 1.0}],
+    }
+    task = w.train(sleep=0)
+    await task
+    ret = task.result()
+    assert ret["output"] is None
+    meta = w.get_metadata()
+    assert "description" in meta
+    assert "id" in meta
+    assert "name" in meta
+    pargs = w.get_predict_args()
+    targs = w.get_train_args()
+    for _arg, val in itertools.chain(pargs.items(), targs.items()):
+        print(val)
+        print(fields.Field)
+        assert isinstance(val, fields.Field)
+
+
+async def test_model_with_not_implemented_attributes_and_wrapper(application, mocks):
+    w = v2_wrapper.ModelWrapper("foo", object(), application)
+
+    with pytest.raises(web.HTTPNotImplemented):
+        await w.predict()
+    with pytest.raises(web.HTTPNotImplemented):
+        await w.train()
+
+    meta = w.get_metadata()
+    assert "description" in meta
+    assert "id" in meta
+    assert "name" in meta
+    pargs = w.get_predict_args()
+    targs = w.get_train_args()
+    for _arg, val in itertools.chain(pargs.items(), targs.items()):
+        assert isinstance(val, fields.Field)
+
+
+async def test_loading_ok(application, mocks):
+    deepaas.model.v2.register_models(application)
+
+    for m in deepaas.model.v2.MODELS.values():
+        assert isinstance(m, v2_wrapper.ModelWrapper)
+
+
+async def test_loading_ok_singleton(application, mocks, monkeypatch):
+    deepaas.model.v2.register_models(application)
+    new_model_name = uuid.uuid4().hex
+    monkeypatch.setattr(
+        deepaas.model.loading,
+        "get_available_models",
+        lambda x: {new_model_name: "barz"},
+    )
+    deepaas.model.v2.register_models(application)
+
+    for name, m in deepaas.model.v2.MODELS.items():
+        assert isinstance(m, v2_wrapper.ModelWrapper)
+        assert name != new_model_name
+
+
+def test_loading_error(application, monkeypatch):
+    monkeypatch.setattr(deepaas.model.loading, "get_available_models", lambda x: {})
+    with pytest.raises(exceptions.NoModelsAvailable):
+        deepaas.model.v2.register_models(application)
diff --git a/deepaas/tests/test_versions_api.py b/deepaas/tests/test_versions_api.py
index 3d192d1f..26cdf99a 100644
--- a/deepaas/tests/test_versions_api.py
+++ b/deepaas/tests/test_versions_api.py
@@ -17,36 +17,39 @@
 
 from aiohttp import web
 import aiohttp_apispec
+import pytest
 
 from deepaas.api import v2
 from deepaas.api import versions
-from deepaas.tests import base
 from deepaas.tests import fake_responses
 
 
-class TestApiVersions(base.TestCase):
-    async def get_application(self):
-        app = web.Application(debug=True)
-        app.add_routes(versions.routes)
+@pytest.fixture
+async def application():
+    app = web.Application()
+    app.add_routes(versions.routes)
 
-        app.add_subapp("/v2", v2.get_app())
+    app.add_subapp("/v2", v2.get_app())
 
-        versions.Versions.versions = {}
+    versions.Versions.versions = {}
 
-        aiohttp_apispec.setup_aiohttp_apispec(
-            app=app, url="/swagger.json", swagger_path="/api"
-        )
+    aiohttp_apispec.setup_aiohttp_apispec(
+        app=app, url="/swagger.json", swagger_path="/api"
+    )
 
-        return app
+    return app
 
-    async def test_get_no_versions(self):
-        ret = await self.client.get("/")
-        self.assertEqual(200, ret.status)
-        self.assertDictEqual(fake_responses.empty_versions, await ret.json())
 
-    async def test_v2_version(self):
-        versions.register_version("stable", v2.get_version)
-        ret = await self.client.get("/")
-        self.assertEqual(200, ret.status)
-        expect = fake_responses.versions
-        self.assertDictEqual(expect, await ret.json())
+async def test_get_no_versions(application, aiohttp_client):
+    client = await aiohttp_client(application)
+    ret = await client.get("/")
+    assert 200 == ret.status
+    assert fake_responses.empty_versions == await ret.json()
+
+
+async def test_v2_version(application, aiohttp_client):
+    versions.register_version("stable", v2.get_version)
+    client = await aiohttp_client(application)
+    ret = await client.get("/")
+    assert 200 == ret.status
+    assert fake_responses.versions == await ret.json()