Repository: ycm-core/ycmd

Issue:
Refactor tests to reuse semantic servers
We currently bring up a new instance of omnisharp, gocode, racerd etc for every single test that needs them. It makes tests take _forever_, especially the omnisharp tests. 

We need to figure out a way to reuse the server instances once they're up without making tests clobber each other. If we only manage to get C# tests to reuse omnisharp, I'll call that a win.

@puremourning @micbou @vheon Any takers? :)



Discussion:
I already have [a implementation](https://github.com/Valloric/ycmd/compare/master...mispencer:PersistentOmnisharp) of this, though it's quite out of date at the moment.

https://github.com/Valloric/ycmd/pull/339 implements this for Omnisharp

@Valloric apparently @mispencer got here first...


Resolving patch:
diff --git a/ycmd/completers/cs/cs_completer.py b/ycmd/completers/cs/cs_completer.py
--- a/ycmd/completers/cs/cs_completer.py
+++ b/ycmd/completers/cs/cs_completer.py
@@ -195,15 +195,6 @@ def GetSubcommandsMap( self ):
          self._SolutionSubcommand( request_data,
                                    method = 'ServerIsReady',
                                    no_request_data = True ) ),
-      'SetOmnisharpPort'                 : ( lambda self, request_data, args:
-         self._SolutionSubcommand( request_data,
-                                   method = '_SetOmnisharpPort',
-                                   port = args[ 0 ],
-                                   no_request_data = True ) ),
-      'GetOmnisharpPort'                 : ( lambda self, request_data, args:
-         self._SolutionSubcommand( request_data,
-                                   method = '_GetOmnisharpPort',
-                                   no_request_data = True ) ),
     }
 
 
@@ -331,7 +322,6 @@ def __init__( self, solution_path, keep_logfiles, desired_omnisharp_port ):
     self._omnisharp_phandle = None
     self._desired_omnisharp_port = desired_omnisharp_port
     self._server_state_lock = threading.RLock()
-    self._external_omnisharp = False
 
 
   def CodeCheck( self, request_data ):
@@ -383,7 +373,6 @@ def _StartServer( self ):
         with open( self._filename_stdout, 'w' ) as fstdout:
           self._omnisharp_phandle = utils.SafePopen(
               command, stdout = fstdout, stderr = fstderr )
-          self._external_omnisharp = False
 
       self._solution_path = path_to_solutionfile
 
@@ -399,7 +388,7 @@ def _StopServer( self ):
       self._TryToStopServer()
 
       # Kill it if it's still up
-      if self.ServerIsRunning() and not self.ServerIsExternal():
+      if self.ServerIsRunning():
         self._logger.info( 'Killing OmniSharp server' )
         self._omnisharp_phandle.kill()
 
@@ -544,27 +533,14 @@ def _DefaultParameters( self, request_data ):
     return parameters
 
 
-  def ServerIsExternal( self ):
-    return self._external_omnisharp
-
-
-  def ServerIsRunning( self, external_check = True ):
+  def ServerIsRunning( self ):
     """ Check if our OmniSharp server is running (process is up)."""
-    if not self.ServerIsExternal():
-      return utils.ProcessIsRunning( self._omnisharp_phandle )
-
-    if self._omnisharp_port is None:
-      return False
-
-    if external_check:
-      return self.ServerIsHealthy()
-
-    return True
+    return utils.ProcessIsRunning( self._omnisharp_phandle )
 
 
   def ServerIsHealthy( self ):
     """ Check if our OmniSharp server is healthy (up and serving)."""
-    if not self.ServerIsRunning( external_check = False ):
+    if not self.ServerIsRunning():
       return False
 
     try:
@@ -575,7 +551,7 @@ def ServerIsHealthy( self ):
 
   def ServerIsReady( self ):
     """ Check if our OmniSharp server is ready (loaded solution file)."""
-    if not self.ServerIsRunning( external_check = False ):
+    if not self.ServerIsRunning():
       return False
 
     try:
@@ -595,19 +571,6 @@ def _ServerLocation( self ):
     return 'http://localhost:' + str( self._omnisharp_port )
 
 
-  def _GetOmnisharpPort( self ):
-    return responses.BuildDisplayMessageResponse( self._omnisharp_port )
-
-
-  def _SetOmnisharpPort( self, port ):
-    with self._server_state_lock:
-      if self.ServerIsRunning():
-        self.StopServer()
-
-      self._omnisharp_port = port
-      self._external_omnisharp = True
-
-
   def _GetResponse( self, handler, parameters = {}, timeout = None ):
     """ Handle communication with server """
     target = urllib.parse.urljoin( self._ServerLocation(), handler )


Test patch:
diff --git a/ycmd/tests/__init__.py b/ycmd/tests/__init__.py
--- a/ycmd/tests/__init__.py
+++ b/ycmd/tests/__init__.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+
+from ycmd.tests.test_utils import SetUpApp
+
+shared_app = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
diff --git a/ycmd/tests/clang/__init__.py b/ycmd/tests/clang/__init__.py
--- a/ycmd/tests/clang/__init__.py
+++ b/ycmd/tests/clang/__init__.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+
+from ycmd import handlers
+from ycmd.tests.test_utils import SetUpApp
+
+shared_app = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
+
+
+def IsolatedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes a unique ycmd application as a parameter. It should be used on tests
+  that change the server state in a irreversible way (ex: a semantic subserver
+  is stopped or restarted) or expect a clean state (ex: no semantic subserver
+  started, no .ycm_extra_conf.py loaded, etc).
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    old_server_state = handlers._server_state
+
+    try:
+      test( SetUpApp(), *args, **kwargs )
+    finally:
+      handlers._server_state = old_server_state
+  return Wrapper
diff --git a/ycmd/tests/clang/clang_handlers_test.py b/ycmd/tests/clang/clang_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/clang/clang_handlers_test.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from ..handlers_test import Handlers_test
-
-
-class Clang_Handlers_test( Handlers_test ):
-
-  def __init__( self ):
-    self._file = __file__
diff --git a/ycmd/tests/clang/diagnostics_test.py b/ycmd/tests/clang/diagnostics_test.py
--- a/ycmd/tests/clang/diagnostics_test.py
+++ b/ycmd/tests/clang/diagnostics_test.py
@@ -23,19 +23,18 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from ...server_utils import SetUpPythonPath
-SetUpPythonPath()
 from hamcrest import ( assert_that, contains, contains_string, has_entries,
                        has_entry, has_items, empty, equal_to )
-from .clang_handlers_test import Clang_Handlers_test
-from ycmd.utils import ReadFile
 from pprint import pprint
 
+from ycmd.tests.clang import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest
+from ycmd.utils import ReadFile
 
-class Clang_Diagnostics_test( Clang_Handlers_test ):
 
-  def ZeroBasedLineAndColumn_test( self ):
-    contents = """
+@SharedYcmd
+def Diagnostics_ZeroBasedLineAndColumn_test( app ):
+  contents = """
 void foo() {
   double baz = "foo";
 }
@@ -43,46 +42,47 @@ def ZeroBasedLineAndColumn_test( self ):
 // Padding to 5 lines
 """
 
-    event_data = self._BuildRequest( compilation_flags = ['-x', 'c++'],
-                                     event_name = 'FileReadyToParse',
-                                     contents = contents,
-                                     filetype = 'cpp' )
-
-    results = self._app.post_json( '/event_notification', event_data ).json
-    assert_that( results,
-                 contains(
-                    has_entries( {
-                      'kind': equal_to( 'ERROR' ),
-                      'text': contains_string( 'cannot initialize' ),
-                      'ranges': contains( has_entries( {
-                        'start': has_entries( {
-                          'line_num': 3,
-                          'column_num': 16,
-                        } ),
-                        'end': has_entries( {
-                          'line_num': 3,
-                          'column_num': 21,
-                        } ),
-                      } ) ),
-                      'location': has_entries( {
+  event_data = BuildRequest( compilation_flags = ['-x', 'c++'],
+                             event_name = 'FileReadyToParse',
+                             contents = contents,
+                             filetype = 'cpp' )
+
+  results = app.post_json( '/event_notification', event_data ).json
+  assert_that( results,
+               contains(
+                  has_entries( {
+                    'kind': equal_to( 'ERROR' ),
+                    'text': contains_string( 'cannot initialize' ),
+                    'ranges': contains( has_entries( {
+                      'start': has_entries( {
+                        'line_num': 3,
+                        'column_num': 16,
+                      } ),
+                      'end': has_entries( {
+                        'line_num': 3,
+                        'column_num': 21,
+                      } ),
+                    } ) ),
+                    'location': has_entries( {
+                      'line_num': 3,
+                      'column_num': 10
+                    } ),
+                    'location_extent': has_entries( {
+                      'start': has_entries( {
+                        'line_num': 3,
+                        'column_num': 10,
+                      } ),
+                      'end': has_entries( {
                         'line_num': 3,
-                        'column_num': 10
+                        'column_num': 13,
                       } ),
-                      'location_extent': has_entries( {
-                        'start': has_entries( {
-                          'line_num': 3,
-                          'column_num': 10,
-                        } ),
-                        'end': has_entries( {
-                          'line_num': 3,
-                          'column_num': 13,
-                        } ),
-                      } )
-                    } ) ) )
-
-
-  def SimpleLocationExtent_test( self ):
-    contents = """
+                    } )
+                  } ) ) )
+
+
+@SharedYcmd
+def Diagnostics_SimpleLocationExtent_test( app ):
+  contents = """
 void foo() {
   baz = 5;
 }
@@ -90,30 +90,31 @@ def SimpleLocationExtent_test( self ):
 // Padding to 5 lines
 """
 
-    event_data = self._BuildRequest( compilation_flags = ['-x', 'c++'],
-                                     event_name = 'FileReadyToParse',
-                                     contents = contents,
-                                     filetype = 'cpp' )
-
-    results = self._app.post_json( '/event_notification', event_data ).json
-    assert_that( results,
-                 contains(
-                    has_entries( {
-                      'location_extent': has_entries( {
-                        'start': has_entries( {
-                          'line_num': 3,
-                          'column_num': 3,
-                        } ),
-                        'end': has_entries( {
-                          'line_num': 3,
-                          'column_num': 6,
-                        } ),
-                      } )
-                    } ) ) )
-
-
-  def PragmaOnceWarningIgnored_test( self ):
-    contents = """
+  event_data = BuildRequest( compilation_flags = ['-x', 'c++'],
+                             event_name = 'FileReadyToParse',
+                             contents = contents,
+                             filetype = 'cpp' )
+
+  results = app.post_json( '/event_notification', event_data ).json
+  assert_that( results,
+               contains(
+                  has_entries( {
+                    'location_extent': has_entries( {
+                      'start': has_entries( {
+                        'line_num': 3,
+                        'column_num': 3,
+                      } ),
+                      'end': has_entries( {
+                        'line_num': 3,
+                        'column_num': 6,
+                      } ),
+                    } )
+                  } ) ) )
+
+
+@SharedYcmd
+def Diagnostics_PragmaOnceWarningIgnored_test( app ):
+  contents = """
 #pragma once
 
 struct Foo {
@@ -124,18 +125,19 @@ def PragmaOnceWarningIgnored_test( self ):
 };
 """
 
-    event_data = self._BuildRequest( compilation_flags = ['-x', 'c++'],
-                                     event_name = 'FileReadyToParse',
-                                     contents = contents,
-                                     filepath = '/foo.h',
-                                     filetype = 'cpp' )
+  event_data = BuildRequest( compilation_flags = ['-x', 'c++'],
+                             event_name = 'FileReadyToParse',
+                             contents = contents,
+                             filepath = '/foo.h',
+                             filetype = 'cpp' )
 
-    response = self._app.post_json( '/event_notification', event_data ).json
-    assert_that( response, empty() )
+  response = app.post_json( '/event_notification', event_data ).json
+  assert_that( response, empty() )
 
 
-  def Works_test( self ):
-    contents = """
+@SharedYcmd
+def Diagnostics_Works_test( app ):
+  contents = """
 struct Foo {
   int x  // semicolon missing here!
   int y;
@@ -144,24 +146,25 @@ def Works_test( self ):
 };
 """
 
-    diag_data = self._BuildRequest( compilation_flags = ['-x', 'c++'],
-                                    line_num = 3,
-                                    contents = contents,
-                                    filetype = 'cpp' )
+  diag_data = BuildRequest( compilation_flags = ['-x', 'c++'],
+                            line_num = 3,
+                            contents = contents,
+                            filetype = 'cpp' )
 
-    event_data = diag_data.copy()
-    event_data.update( {
-      'event_name': 'FileReadyToParse',
-    } )
+  event_data = diag_data.copy()
+  event_data.update( {
+    'event_name': 'FileReadyToParse',
+  } )
 
-    self._app.post_json( '/event_notification', event_data )
-    results = self._app.post_json( '/detailed_diagnostic', diag_data ).json
-    assert_that( results,
-                 has_entry( 'message', contains_string( "expected ';'" ) ) )
+  app.post_json( '/event_notification', event_data )
+  results = app.post_json( '/detailed_diagnostic', diag_data ).json
+  assert_that( results,
+               has_entry( 'message', contains_string( "expected ';'" ) ) )
 
 
-  def Multiline_test( self ):
-    contents = """
+@SharedYcmd
+def Diagnostics_Multiline_test( app ):
+  contents = """
 struct Foo {
   Foo(int z) {}
 };
@@ -171,49 +174,50 @@ def Multiline_test( self ):
 }
 """
 
-    diag_data = self._BuildRequest( compilation_flags = [ '-x', 'c++' ],
-                                    line_num = 7,
-                                    contents = contents,
-                                    filetype = 'cpp' )
-
-    event_data = diag_data.copy()
-    event_data.update( {
-      'event_name': 'FileReadyToParse',
-    } )
-
-    self._app.post_json( '/event_notification', event_data )
-    results = self._app.post_json( '/detailed_diagnostic', diag_data ).json
-    assert_that( results,
-                 has_entry( 'message', contains_string( "\n" ) ) )
-
-
-  def FixIt_Available_test( self ):
-    contents = ReadFile( self._PathToTestFile( 'FixIt_Clang_cpp11.cpp' ) )
-
-    event_data = self._BuildRequest( contents = contents,
-                                     event_name = 'FileReadyToParse',
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++',
-                                                           '-std=c++03',
-                                                           '-Wall',
-                                                           '-Wextra',
-                                                           '-pedantic' ] )
-
-    response = self._app.post_json( '/event_notification', event_data ).json
-
-    pprint( response )
-
-    assert_that( response, has_items(
-      has_entries( {
-        'location': has_entries( { 'line_num': 16, 'column_num': 3 } ),
-        'text': equal_to( 'switch condition type \'A\' '
-                          'requires explicit conversion to \'int\''),
-        'fixit_available': True
-      } ),
-      has_entries( {
-        'location': has_entries( { 'line_num': 11, 'column_num': 3 } ),
-        'text': equal_to(
-           'explicit conversion functions are a C++11 extension' ),
-        'fixit_available': False
-      } ),
-    ) )
+  diag_data = BuildRequest( compilation_flags = [ '-x', 'c++' ],
+                            line_num = 7,
+                            contents = contents,
+                            filetype = 'cpp' )
+
+  event_data = diag_data.copy()
+  event_data.update( {
+    'event_name': 'FileReadyToParse',
+  } )
+
+  app.post_json( '/event_notification', event_data )
+  results = app.post_json( '/detailed_diagnostic', diag_data ).json
+  assert_that( results,
+               has_entry( 'message', contains_string( "\n" ) ) )
+
+
+@SharedYcmd
+def Diagnostics_FixIt_Available_test( app ):
+  contents = ReadFile( PathToTestFile( 'FixIt_Clang_cpp11.cpp' ) )
+
+  event_data = BuildRequest( contents = contents,
+                             event_name = 'FileReadyToParse',
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++',
+                                                   '-std=c++03',
+                                                   '-Wall',
+                                                   '-Wextra',
+                                                   '-pedantic' ] )
+
+  response = app.post_json( '/event_notification', event_data ).json
+
+  pprint( response )
+
+  assert_that( response, has_items(
+    has_entries( {
+      'location': has_entries( { 'line_num': 16, 'column_num': 3 } ),
+      'text': equal_to( 'switch condition type \'A\' '
+                        'requires explicit conversion to \'int\''),
+      'fixit_available': True
+    } ),
+    has_entries( {
+      'location': has_entries( { 'line_num': 11, 'column_num': 3 } ),
+      'text': equal_to(
+         'explicit conversion functions are a C++11 extension' ),
+      'fixit_available': False
+    } ),
+  ) )
diff --git a/ycmd/tests/clang/get_completions_test.py b/ycmd/tests/clang/get_completions_test.py
--- a/ycmd/tests/clang/get_completions_test.py
+++ b/ycmd/tests/clang/get_completions_test.py
@@ -23,257 +23,259 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from webtest import TestApp
 from nose.tools import eq_
 from hamcrest import ( assert_that, contains, contains_inanyorder, empty,
                        has_item, has_items, has_entry, has_entries )
-from ...responses import UnknownExtraConf, NoExtraConfDetected
-from ... import handlers
-from ycmd.completers.cpp.clang_completer import NO_COMPLETIONS_MESSAGE
-from .clang_handlers_test import Clang_Handlers_test
-from ycmd.utils import ReadFile
 import http.client
 
+from ycmd.completers.cpp.clang_completer import NO_COMPLETIONS_MESSAGE
+from ycmd.responses import UnknownExtraConf, NoExtraConfDetected
+from ycmd.tests.clang import IsolatedYcmd, PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher,
+                                    ErrorMatcher, UserOption )
+from ycmd.utils import ReadFile
 
-class Clang_GetCompletions_test( Clang_Handlers_test ):
-
-  def __init__( self ):
-    super( Clang_GetCompletions_test, self ).__init__()
-    self._no_completions_error = self._ErrorMatcher( RuntimeError,
-                                                     NO_COMPLETIONS_MESSAGE )
-
-
-  def _RunTest( self, test ):
-    """
-    Method to run a simple completion test and verify the result
-
-    Note: uses the .ycm_extra_conf from general_fallback/ which:
-     - supports cpp, c and objc
-     - requires extra_conf_data containing 'filetype&' = the filetype
-
-    this should be sufficient for many standard test cases
-
-    test is a dictionary containing:
-      'request': kwargs for BuildRequest
-      'expect': {
-         'response': server response code (e.g. httplib.OK)
-         'data': matcher for the server response json
-      }
-    """
-    self._app.post_json( '/load_extra_conf_file', {
-      'filepath': self._PathToTestFile( 'general_fallback',
-                                        '.ycm_extra_conf.py' ) } )
-
-
-    contents = ReadFile( test[ 'request' ][ 'filepath' ] )
-
-    def CombineRequest( request, data ):
-      kw = request
-      request.update( data )
-      return self._BuildRequest( **kw )
-
-    # Because we aren't testing this command, we *always* ignore errors. This
-    # is mainly because we (may) want to test scenarios where the completer
-    # throws an exception and the easiest way to do that is to throw from
-    # within the FlagsForFile function.
-    self._app.post_json( '/event_notification',
-                         CombineRequest( test[ 'request' ], {
-                           'event_name': 'FileReadyToParse',
-                           'contents': contents,
-                         } ),
-                         expect_errors = True )
-
-    # We also ignore errors here, but then we check the response code ourself.
-    # This is to allow testing of requests returning errors.
-    response = self._app.post_json( '/completions',
-                                    CombineRequest( test[ 'request' ], {
-                                      'contents': contents
-                                    } ),
-                                    expect_errors = True )
-
-    eq_( response.status_code, test[ 'expect' ][ 'response' ] )
-
-    assert_that( response.json, test[ 'expect' ][ 'data' ] )
-
-
-  def ForcedWithNoTrigger_test( self ):
-    self._RunTest( {
-      'description': 'semantic completion with force query=DO_SO',
-      'request': {
-        'filetype'  : 'cpp',
-        'filepath'  : self._PathToTestFile( 'general_fallback',
-                                            'lang_cpp.cc' ),
-        'line_num'  : 54,
-        'column_num': 8,
-        'extra_conf_data': { '&filetype': 'cpp' },
-        'force_semantic': True,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains(
-            self._CompletionEntryMatcher( 'DO_SOMETHING_TO', 'void' ),
-            self._CompletionEntryMatcher( 'DO_SOMETHING_WITH', 'void' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
-
-
-  def Fallback_NoSuggestions_test( self ):
-    # TESTCASE1 (general_fallback/lang_c.c)
-    self._RunTest( {
-      'description': 'Triggered, fallback but no query so no completions',
-      'request': {
-        'filetype'  : 'c',
-        'filepath'  : self._PathToTestFile( 'general_fallback', 'lang_c.c' ),
-        'line_num'  : 29,
-        'column_num': 21,
-        'extra_conf_data': { '&filetype': 'c' },
-        'force_semantic': False,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': empty(),
-          'errors': has_item( self._ErrorMatcher( RuntimeError,
-                                                  NO_COMPLETIONS_MESSAGE ) ),
-        } )
-      },
-    } )
-
-
-  def Fallback_NoSuggestions_MinimumCharaceters_test( self ):
-    # TESTCASE1 (general_fallback/lang_cpp.cc)
-    self._RunTest( {
-      'description': 'fallback general completion obeys min chars setting '
-                     ' (query="a")',
-      'request': {
-        'filetype'  : 'cpp',
-        'filepath'  : self._PathToTestFile( 'general_fallback',
-                                            'lang_cpp.cc' ),
-        'line_num'  : 21,
-        'column_num': 22,
-        'extra_conf_data': { '&filetype': 'cpp' },
-        'force_semantic': False,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': empty(),
-          'errors': has_item( self._no_completions_error ),
-        } )
-      },
-    } )
-
-
-  def Fallback_Suggestions_test( self ):
-    # TESTCASE1 (general_fallback/lang_c.c)
-    self._RunTest( {
-      'description': '. after macro with some query text (.a_)',
-      'request': {
-        'filetype'  : 'c',
-        'filepath'  : self._PathToTestFile( 'general_fallback', 'lang_c.c' ),
-        'line_num'  : 29,
-        'column_num': 23,
-        'extra_conf_data': { '&filetype': 'c' },
-        'force_semantic': False,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': has_item( self._CompletionEntryMatcher( 'a_parameter',
-                                                                 '[ID]' ) ),
-          'errors': has_item( self._no_completions_error ),
-        } )
-      },
-    } )
-
-
-  def Fallback_Exception_test( self ):
-    # TESTCASE4 (general_fallback/lang_c.c)
-    # extra conf throws exception
-    self._RunTest( {
-      'description': '. on struct returns identifier because of error',
-      'request': {
-        'filetype'  : 'c',
-        'filepath'  : self._PathToTestFile( 'general_fallback', 'lang_c.c' ),
-        'line_num'  : 62,
-        'column_num': 20,
-        'extra_conf_data': { '&filetype': 'c', 'throw': 'testy' },
-        'force_semantic': False,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains(
-            self._CompletionEntryMatcher( 'a_parameter', '[ID]' ),
-            self._CompletionEntryMatcher( 'another_parameter', '[ID]' ),
-          ),
-          'errors': has_item( self._ErrorMatcher( ValueError, 'testy' ) )
-        } )
-      },
-    } )
-
-
-  def Forced_NoFallback_test( self ):
-    # TESTCASE2 (general_fallback/lang_c.c)
-    self._RunTest( {
-      'description': '-> after macro with forced semantic',
-      'request': {
-        'filetype'  : 'c',
-        'filepath'  : self._PathToTestFile( 'general_fallback', 'lang_c.c' ),
-        'line_num'  : 41,
-        'column_num': 30,
-        'extra_conf_data': { '&filetype': 'c' },
-        'force_semantic': True,
-      },
-      'expect': {
-        'response': http.client.INTERNAL_SERVER_ERROR,
-        'data': self._no_completions_error,
-      },
-    } )
-
-
-  def FilteredNoResults_Fallback_test( self ):
-    # no errors because the semantic completer returned results, but they
-    # were filtered out by the query, so this is considered working OK
-    # (whereas no completions from the semantic engine is considered an
-    # error)
-
-    # TESTCASE5 (general_fallback/lang_cpp.cc)
-    self._RunTest( {
-      'description': '. on struct returns IDs after query=do_',
-      'request': {
-        'filetype':   'c',
-        'filepath':   self._PathToTestFile( 'general_fallback', 'lang_c.c' ),
-        'line_num':   71,
-        'column_num': 18,
-        'extra_conf_data': { '&filetype': 'c' },
-        'force_semantic': False,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            # do_ is an identifier because it is already in the file when we
-            # load it
-            self._CompletionEntryMatcher( 'do_', '[ID]' ),
-            self._CompletionEntryMatcher( 'do_something', '[ID]' ),
-            self._CompletionEntryMatcher( 'do_another_thing', '[ID]' ),
-          ),
-          'errors': empty()
-        } )
-      },
-    } )
-
-
-  def WorksWithExplicitFlags_test( self ):
-    self._app.post_json(
-      '/ignore_extra_conf_file',
-      { 'filepath': self._PathToTestFile( '.ycm_extra_conf.py' ) } )
-    contents = """
+NO_COMPLETIONS_ERROR = ErrorMatcher( RuntimeError, NO_COMPLETIONS_MESSAGE )
+
+
+def RunTest( app, test ):
+  """
+  Method to run a simple completion test and verify the result
+
+  Note: uses the .ycm_extra_conf from general_fallback/ which:
+   - supports cpp, c and objc
+   - requires extra_conf_data containing 'filetype&' = the filetype
+
+  this should be sufficient for many standard test cases
+
+  test is a dictionary containing:
+    'request': kwargs for BuildRequest
+    'expect': {
+       'response': server response code (e.g. httplib.OK)
+       'data': matcher for the server response json
+    }
+  """
+  app.post_json( '/load_extra_conf_file', {
+    'filepath': PathToTestFile( 'general_fallback',
+                                '.ycm_extra_conf.py' ) } )
+
+
+  contents = ReadFile( test[ 'request' ][ 'filepath' ] )
+
+  def CombineRequest( request, data ):
+    kw = request
+    request.update( data )
+    return BuildRequest( **kw )
+
+  # Because we aren't testing this command, we *always* ignore errors. This
+  # is mainly because we (may) want to test scenarios where the completer
+  # throws an exception and the easiest way to do that is to throw from
+  # within the FlagsForFile function.
+  app.post_json( '/event_notification',
+                 CombineRequest( test[ 'request' ], {
+                   'event_name': 'FileReadyToParse',
+                   'contents': contents,
+                 } ),
+                 expect_errors = True )
+
+  # We also ignore errors here, but then we check the response code ourself.
+  # This is to allow testing of requests returning errors.
+  response = app.post_json( '/completions',
+                            CombineRequest( test[ 'request' ], {
+                              'contents': contents
+                            } ),
+                            expect_errors = True )
+
+  eq_( response.status_code, test[ 'expect' ][ 'response' ] )
+
+  assert_that( response.json, test[ 'expect' ][ 'data' ] )
+
+
+@SharedYcmd
+def GetCompletions_ForcedWithNoTrigger_test( app ):
+  RunTest( app, {
+    'description': 'semantic completion with force query=DO_SO',
+    'request': {
+      'filetype'  : 'cpp',
+      'filepath'  : PathToTestFile( 'general_fallback',
+                                    'lang_cpp.cc' ),
+      'line_num'  : 54,
+      'column_num': 8,
+      'extra_conf_data': { '&filetype': 'cpp' },
+      'force_semantic': True,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains(
+          CompletionEntryMatcher( 'DO_SOMETHING_TO', 'void' ),
+          CompletionEntryMatcher( 'DO_SOMETHING_WITH', 'void' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_Fallback_NoSuggestions_test( app ):
+  # TESTCASE1 (general_fallback/lang_c.c)
+  RunTest( app, {
+    'description': 'Triggered, fallback but no query so no completions',
+    'request': {
+      'filetype'  : 'c',
+      'filepath'  : PathToTestFile( 'general_fallback', 'lang_c.c' ),
+      'line_num'  : 29,
+      'column_num': 21,
+      'extra_conf_data': { '&filetype': 'c' },
+      'force_semantic': False,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': empty(),
+        'errors': has_item( NO_COMPLETIONS_ERROR ),
+      } )
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_Fallback_NoSuggestions_MinimumCharaceters_test( app ):
+  # TESTCASE1 (general_fallback/lang_cpp.cc)
+  RunTest( app, {
+    'description': 'fallback general completion obeys min chars setting '
+                   ' (query="a")',
+    'request': {
+      'filetype'  : 'cpp',
+      'filepath'  : PathToTestFile( 'general_fallback',
+                                    'lang_cpp.cc' ),
+      'line_num'  : 21,
+      'column_num': 22,
+      'extra_conf_data': { '&filetype': 'cpp' },
+      'force_semantic': False,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': empty(),
+        'errors': has_item( NO_COMPLETIONS_ERROR ),
+      } )
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_Fallback_Suggestions_test( app ):
+  # TESTCASE1 (general_fallback/lang_c.c)
+  RunTest( app, {
+    'description': '. after macro with some query text (.a_)',
+    'request': {
+      'filetype'  : 'c',
+      'filepath'  : PathToTestFile( 'general_fallback', 'lang_c.c' ),
+      'line_num'  : 29,
+      'column_num': 23,
+      'extra_conf_data': { '&filetype': 'c' },
+      'force_semantic': False,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': has_item( CompletionEntryMatcher( 'a_parameter',
+                                                         '[ID]' ) ),
+        'errors': has_item( NO_COMPLETIONS_ERROR ),
+      } )
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_Fallback_Exception_test( app ):
+  # TESTCASE4 (general_fallback/lang_c.c)
+  # extra conf throws exception
+  RunTest( app, {
+    'description': '. on struct returns identifier because of error',
+    'request': {
+      'filetype'  : 'c',
+      'filepath'  : PathToTestFile( 'general_fallback', 'lang_c.c' ),
+      'line_num'  : 62,
+      'column_num': 20,
+      'extra_conf_data': { '&filetype': 'c', 'throw': 'testy' },
+      'force_semantic': False,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains(
+          CompletionEntryMatcher( 'a_parameter', '[ID]' ),
+          CompletionEntryMatcher( 'another_parameter', '[ID]' ),
+        ),
+        'errors': has_item( ErrorMatcher( ValueError, 'testy' ) )
+      } )
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_Forced_NoFallback_test( app ):
+  # TESTCASE2 (general_fallback/lang_c.c)
+  RunTest( app, {
+    'description': '-> after macro with forced semantic',
+    'request': {
+      'filetype'  : 'c',
+      'filepath'  : PathToTestFile( 'general_fallback', 'lang_c.c' ),
+      'line_num'  : 41,
+      'column_num': 30,
+      'extra_conf_data': { '&filetype': 'c' },
+      'force_semantic': True,
+    },
+    'expect': {
+      'response': http.client.INTERNAL_SERVER_ERROR,
+      'data': NO_COMPLETIONS_ERROR,
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_FilteredNoResults_Fallback_test( app ):
+  # no errors because the semantic completer returned results, but they
+  # were filtered out by the query, so this is considered working OK
+  # (whereas no completions from the semantic engine is considered an
+  # error)
+
+  # TESTCASE5 (general_fallback/lang_cpp.cc)
+  RunTest( app, {
+    'description': '. on struct returns IDs after query=do_',
+    'request': {
+      'filetype':   'c',
+      'filepath':   PathToTestFile( 'general_fallback', 'lang_c.c' ),
+      'line_num':   71,
+      'column_num': 18,
+      'extra_conf_data': { '&filetype': 'c' },
+      'force_semantic': False,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          # do_ is an identifier because it is already in the file when we
+          # load it
+          CompletionEntryMatcher( 'do_', '[ID]' ),
+          CompletionEntryMatcher( 'do_something', '[ID]' ),
+          CompletionEntryMatcher( 'do_another_thing', '[ID]' ),
+        ),
+        'errors': empty()
+      } )
+    },
+  } )
+
+
+@SharedYcmd
+def GetCompletions_WorksWithExplicitFlags_test( app ):
+  app.post_json(
+    '/ignore_extra_conf_file',
+    { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } )
+  contents = """
 struct Foo {
   int x;
   int y;
@@ -287,28 +289,28 @@ def WorksWithExplicitFlags_test( self ):
 }
 """
 
-    completion_data = self._BuildRequest( filepath = '/foo.cpp',
-                                          filetype = 'cpp',
-                                          contents = contents,
-                                          line_num = 11,
-                                          column_num = 7,
-                                          compilation_flags = ['-x', 'c++'] )
-
-    response_data = self._app.post_json( '/completions', completion_data ).json
-    assert_that( response_data[ 'completions'],
-                 has_items( self._CompletionEntryMatcher( 'c' ),
-                            self._CompletionEntryMatcher( 'x' ),
-                            self._CompletionEntryMatcher( 'y' ) ) )
-    eq_( 7, response_data[ 'completion_start_column' ] )
-
-
-  def NoCompletionsWhenAutoTriggerOff_test( self ):
-    with self.UserOption( 'auto_trigger', False ):
-      self._app = TestApp( handlers.app )
-      self._app.post_json(
-        '/ignore_extra_conf_file',
-        { 'filepath': self._PathToTestFile( '.ycm_extra_conf.py' ) } )
-      contents = """
+  completion_data = BuildRequest( filepath = '/foo.cpp',
+                                  filetype = 'cpp',
+                                  contents = contents,
+                                  line_num = 11,
+                                  column_num = 7,
+                                  compilation_flags = ['-x', 'c++'] )
+
+  response_data = app.post_json( '/completions', completion_data ).json
+  assert_that( response_data[ 'completions'],
+               has_items( CompletionEntryMatcher( 'c' ),
+                          CompletionEntryMatcher( 'x' ),
+                          CompletionEntryMatcher( 'y' ) ) )
+  eq_( 7, response_data[ 'completion_start_column' ] )
+
+
+@SharedYcmd
+def GetCompletions_NoCompletionsWhenAutoTriggerOff_test( app ):
+  with UserOption( 'auto_trigger', False ):
+    app.post_json(
+      '/ignore_extra_conf_file',
+      { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } )
+    contents = """
 struct Foo {
   int x;
   int y;
@@ -322,97 +324,101 @@ def NoCompletionsWhenAutoTriggerOff_test( self ):
 }
 """
 
-      completion_data = self._BuildRequest( filepath = '/foo.cpp',
-                                            filetype = 'cpp',
-                                            contents = contents,
-                                            line_num = 11,
-                                            column_num = 7,
-                                            compilation_flags = ['-x', 'c++'] )
-
-      results = self._app.post_json( '/completions',
-                                    completion_data ).json[ 'completions' ]
-      assert_that( results, empty() )
-
-
-  def UnknownExtraConfException_test( self ):
-    filepath = self._PathToTestFile( 'basic.cpp' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'cpp',
-                                          contents = ReadFile( filepath ),
-                                          line_num = 11,
-                                          column_num = 7,
-                                          force_semantic = True )
-
-    response = self._app.post_json( '/completions',
-                                    completion_data,
-                                    expect_errors = True )
-
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-    assert_that( response.json,
-                 has_entry( 'exception',
-                            has_entry( 'TYPE', UnknownExtraConf.__name__ ) ) )
-
-    self._app.post_json(
-      '/ignore_extra_conf_file',
-      { 'filepath': self._PathToTestFile( '.ycm_extra_conf.py' ) } )
-
-    response = self._app.post_json( '/completions',
-                                    completion_data,
-                                    expect_errors = True )
-
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-    assert_that( response.json,
-                 has_entry( 'exception',
-                            has_entry( 'TYPE',
-                                       NoExtraConfDetected.__name__ ) ) )
-
-
-  def WorksWhenExtraConfExplicitlyAllowed_test( self ):
-    self._app.post_json(
-      '/load_extra_conf_file',
-      { 'filepath': self._PathToTestFile( '.ycm_extra_conf.py' ) } )
-
-    filepath = self._PathToTestFile( 'basic.cpp' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'cpp',
-                                          contents = ReadFile( filepath ),
-                                          line_num = 11,
-                                          column_num = 7 )
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that( results, has_items( self._CompletionEntryMatcher( 'c' ),
-                                     self._CompletionEntryMatcher( 'x' ),
-                                     self._CompletionEntryMatcher( 'y' ) ) )
-
-
-  def ExceptionWhenNoFlagsFromExtraConf_test( self ):
-    self._app.post_json(
-      '/load_extra_conf_file',
-      { 'filepath': self._PathToTestFile( 'noflags',
-                                          '.ycm_extra_conf.py' ) } )
-
-    filepath = self._PathToTestFile( 'noflags', 'basic.cpp' )
-
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'cpp',
-                                          contents = ReadFile( filepath ),
-                                          line_num = 11,
-                                          column_num = 7,
-                                          force_semantic = True )
-
-    response = self._app.post_json( '/completions',
-                                    completion_data,
-                                    expect_errors = True )
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-
-    assert_that( response.json,
-                 has_entry( 'exception',
-                            has_entry( 'TYPE', RuntimeError.__name__ ) ) )
-
-
-  def ForceSemantic_OnlyFileteredCompletions_test( self ):
-    contents = """
+    completion_data = BuildRequest( filepath = '/foo.cpp',
+                                    filetype = 'cpp',
+                                    contents = contents,
+                                    line_num = 11,
+                                    column_num = 7,
+                                    compilation_flags = ['-x', 'c++'] )
+
+    results = app.post_json( '/completions',
+                             completion_data ).json[ 'completions' ]
+    assert_that( results, empty() )
+
+
+@IsolatedYcmd
+def GetCompletions_UnknownExtraConfException_test( app ):
+  filepath = PathToTestFile( 'basic.cpp' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'cpp',
+                                  contents = ReadFile( filepath ),
+                                  line_num = 11,
+                                  column_num = 7,
+                                  force_semantic = True )
+
+  response = app.post_json( '/completions',
+                            completion_data,
+                            expect_errors = True )
+
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+  assert_that( response.json,
+               has_entry( 'exception',
+                          has_entry( 'TYPE', UnknownExtraConf.__name__ ) ) )
+
+  app.post_json(
+    '/ignore_extra_conf_file',
+    { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } )
+
+  response = app.post_json( '/completions',
+                            completion_data,
+                            expect_errors = True )
+
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+  assert_that( response.json,
+               has_entry( 'exception',
+                          has_entry( 'TYPE',
+                                     NoExtraConfDetected.__name__ ) ) )
+
+
+@IsolatedYcmd
+def GetCompletions_WorksWhenExtraConfExplicitlyAllowed_test( app ):
+  app.post_json(
+    '/load_extra_conf_file',
+    { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } )
+
+  filepath = PathToTestFile( 'basic.cpp' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'cpp',
+                                  contents = ReadFile( filepath ),
+                                  line_num = 11,
+                                  column_num = 7 )
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that( results, has_items( CompletionEntryMatcher( 'c' ),
+                                   CompletionEntryMatcher( 'x' ),
+                                   CompletionEntryMatcher( 'y' ) ) )
+
+
+@SharedYcmd
+def GetCompletions_ExceptionWhenNoFlagsFromExtraConf_test( app ):
+  app.post_json(
+    '/load_extra_conf_file',
+    { 'filepath': PathToTestFile( 'noflags',
+                                  '.ycm_extra_conf.py' ) } )
+
+  filepath = PathToTestFile( 'noflags', 'basic.cpp' )
+
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'cpp',
+                                  contents = ReadFile( filepath ),
+                                  line_num = 11,
+                                  column_num = 7,
+                                  force_semantic = True )
+
+  response = app.post_json( '/completions',
+                            completion_data,
+                            expect_errors = True )
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+
+  assert_that( response.json,
+               has_entry( 'exception',
+                          has_entry( 'TYPE', RuntimeError.__name__ ) ) )
+
+
+@SharedYcmd
+def GetCompletions_ForceSemantic_OnlyFilteredCompletions_test( app ):
+  contents = """
 int main()
 {
   int foobar;
@@ -424,64 +430,66 @@ def ForceSemantic_OnlyFileteredCompletions_test( self ):
 }
 """
 
-    completion_data = self._BuildRequest( filepath = '/foo.cpp',
-                                          filetype = 'cpp',
-                                          force_semantic = True,
-                                          contents = contents,
-                                          line_num = 9,
-                                          column_num = 8,
-                                          compilation_flags = ['-x', 'c++'] )
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that(
-      results,
-      contains_inanyorder( self._CompletionEntryMatcher( 'foobar' ),
-                           self._CompletionEntryMatcher( 'floozar' ) )
-    )
-
-
-  def ClientDataGivenToExtraConf_test( self ):
-    self._app.post_json(
-      '/load_extra_conf_file',
-      { 'filepath': self._PathToTestFile( 'client_data',
-                                          '.ycm_extra_conf.py' ) } )
-
-    filepath = self._PathToTestFile( 'client_data', 'main.cpp' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'cpp',
-                                          contents = ReadFile( filepath ),
-                                          line_num = 9,
-                                          column_num = 7,
-                                          extra_conf_data = {
-                                            'flags': ['-x', 'c++']
-                                          } )
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that( results, has_item( self._CompletionEntryMatcher( 'x' ) ) )
-
-
-  def FilenameCompleter_ClientDataGivenToExtraConf_test( self ):
-    self._app.post_json(
-      '/load_extra_conf_file',
-      { 'filepath': self._PathToTestFile( 'client_data',
-                                          '.ycm_extra_conf.py' ) } )
-
-    filepath = self._PathToTestFile( 'client_data', 'include.cpp' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'cpp',
-                                          contents = ReadFile( filepath ),
-                                          line_num = 1,
-                                          column_num = 11,
-                                          extra_conf_data = {
-                                            'flags': ['-x', 'c++']
-                                          } )
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that(
-      results,
-      has_item( self._CompletionEntryMatcher( 'include.hpp',
-                extra_menu_info = '[File]' ) )
-    )
+  completion_data = BuildRequest( filepath = '/foo.cpp',
+                                  filetype = 'cpp',
+                                  force_semantic = True,
+                                  contents = contents,
+                                  line_num = 9,
+                                  column_num = 8,
+                                  compilation_flags = ['-x', 'c++'] )
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that(
+    results,
+    contains_inanyorder( CompletionEntryMatcher( 'foobar' ),
+                         CompletionEntryMatcher( 'floozar' ) )
+  )
+
+
+@SharedYcmd
+def GetCompletions_ClientDataGivenToExtraConf_test( app ):
+  app.post_json(
+    '/load_extra_conf_file',
+    { 'filepath': PathToTestFile( 'client_data',
+                                  '.ycm_extra_conf.py' ) } )
+
+  filepath = PathToTestFile( 'client_data', 'main.cpp' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'cpp',
+                                  contents = ReadFile( filepath ),
+                                  line_num = 9,
+                                  column_num = 7,
+                                  extra_conf_data = {
+                                    'flags': ['-x', 'c++']
+                                  } )
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that( results, has_item( CompletionEntryMatcher( 'x' ) ) )
+
+
+@SharedYcmd
+def GetCompletions_FilenameCompleter_ClientDataGivenToExtraConf_test( app ):
+  app.post_json(
+    '/load_extra_conf_file',
+    { 'filepath': PathToTestFile( 'client_data',
+                                  '.ycm_extra_conf.py' ) } )
+
+  filepath = PathToTestFile( 'client_data', 'include.cpp' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'cpp',
+                                  contents = ReadFile( filepath ),
+                                  line_num = 1,
+                                  column_num = 11,
+                                  extra_conf_data = {
+                                    'flags': ['-x', 'c++']
+                                  } )
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that(
+    results,
+    has_item( CompletionEntryMatcher( 'include.hpp',
+              extra_menu_info = '[File]' ) )
+  )
diff --git a/ycmd/tests/clang/subcommands_test.py b/ycmd/tests/clang/subcommands_test.py
--- a/ycmd/tests/clang/subcommands_test.py
+++ b/ycmd/tests/clang/subcommands_test.py
@@ -23,692 +23,698 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from webtest import AppError
-from nose.tools import eq_
 from hamcrest import ( assert_that, calling, contains, equal_to,
                        has_entries, raises )
-from ycmd.completers.cpp.clang_completer import NO_DOCUMENTATION_MESSAGE
-from .clang_handlers_test import Clang_Handlers_test
-from ycmd.utils import ReadFile
+from nose.tools import eq_
 from pprint import pprint
-import os.path
+from webtest import AppError
 import http.client
+import os.path
 
+from ycmd.completers.cpp.clang_completer import NO_DOCUMENTATION_MESSAGE
+from ycmd.tests.clang import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, ErrorMatcher
+from ycmd.utils import ReadFile
 
-class Clang_Subcommands_test( Clang_Handlers_test ):
-
-  def GoTo_ZeroBasedLineAndColumn_test( self ):
-    contents = ReadFile( self._PathToTestFile(
-      'GoTo_Clang_ZeroBasedLineAndColumn_test.cc' ) )
-
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = ['GoToDefinition'],
-                                    compilation_flags = ['-x', 'c++'],
-                                    line_num = 10,
-                                    column_num = 3,
-                                    contents = contents,
-                                    filetype = 'cpp' )
-
-    eq_( {
-      'filepath': os.path.abspath( '/foo' ),
-      'line_num': 2,
-      'column_num': 8
-    }, self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def _GoTo_all( self, filename, command, test ):
-    contents = ReadFile( self._PathToTestFile( filename ) )
-    common_request = {
-      'completer_target' : 'filetype_default',
-      'command_arguments': command,
-      'compilation_flags': ['-x',
-                            'c++'],
-      'line_num'         : 10,
-      'column_num'       : 3,
-      'contents'         : contents,
-      'filetype'         : 'cpp'
-    }
-    common_response = {
-      'filepath': os.path.abspath( '/foo' ),
-    }
-
-    request = common_request
-    request.update( {
-        'line_num'  : test['request'][0],
-        'column_num': test['request'][1],
-    })
-    response = common_response
-    response.update({
-        'line_num'  : test['response'][0],
-        'column_num': test['response'][1],
-    })
-
-    goto_data = self._BuildRequest( **request )
-
-    eq_( response,
-         self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoTo_all_test( self ):
-    # GoToDeclaration
-    tests = [
-      # Local::x -> declaration of x
-      { 'request': [23, 21], 'response': [ 4,  9] },
-      # Local::in_line -> declaration of Local::in_line
-      { 'request': [24, 26], 'response': [ 6, 10] },
-      # Local -> declaration of Local
-      { 'request': [24, 16], 'response': [ 2, 11] },
-      # Local::out_of_line -> declaration of Local::out_of_line
-      { 'request': [25, 27], 'response': [11, 10] },
-      # GoToDeclaration on definition of out_of_line moves to declaration
-      { 'request': [14, 13], 'response': [11, 10] },
-      # main -> declaration of main
-      { 'request': [21,  7], 'response': [19, 5] },
-    ]
-
-    for test in tests:
-      yield ( self._GoTo_all,
-              'GoTo_all_Clang_test.cc',
-              [ 'GoToDeclaration' ],
-              test )
-
-    # GoToDefinition - identical to GoToDeclaration
-    #
-    # The semantics of this seem the wrong way round to me. GoToDefinition
-    # should go to where a method is implemented, not where it is declared.
-    #
-    tests = [
-      # Local::x -> declaration of x
-      { 'request': [23, 21], 'response': [ 4,  9] },
-      # Local::in_line -> declaration of Local::in_line
-      { 'request': [24, 26], 'response': [ 6, 10] },
-      # Local -> declaration of Local
-      { 'request': [24, 16], 'response': [ 2, 11] },
-      # sic: Local::out_of_line -> definition of Local::out_of_line
-      { 'request': [25, 27], 'response': [14, 13] }, # sic
-      # sic: GoToDeclaration on definition of out_of_line moves to itself
-      { 'request': [14, 13], 'response': [14, 13] }, # sic
-      # main -> definition of main (not declaration)
-      { 'request': [21,  7], 'response': [21, 5] }, # sic
-    ]
-
-    for test in tests:
-      yield ( self._GoTo_all,
-              'GoTo_all_Clang_test.cc',
-              [ 'GoToDefinition' ],
-              test )
-
-    # GoTo - identical to GoToDeclaration
-    #
-    # The semantics of this seem the wrong way round to me. GoToDefinition
-    # should go to where a method is implemented, not where it is declared.
-    #
-    tests = [
-      # Local::x -> declaration of x
-      { 'request': [23, 21], 'response': [ 4,  9] },
-      # Local::in_line -> declaration of Local::in_line
-      { 'request': [24, 26], 'response': [ 6, 10] },
-      # Local -> declaration of Local
-      { 'request': [24, 16], 'response': [ 2, 11] },
-      # sic: Local::out_of_line -> definition of Local::out_of_line
-      { 'request': [25, 27], 'response': [14, 13] }, # sic
-      # sic: GoToDeclaration on definition of out_of_line moves to itself
-      { 'request': [14, 13], 'response': [14, 13] }, # sic
-      # main -> definition of main (not declaration)
-      { 'request': [21,  7], 'response': [21, 5] }, # sic
-    ]
-
-    for test in tests:
-      yield ( self._GoTo_all,
-              'GoTo_all_Clang_test.cc',
-              [ 'GoTo' ],
-              test )
-
-    # GoToImprecise - identical to GoToDeclaration
-    #
-    # The semantics of this seem the wrong way round to me. GoToDefinition
-    # should go to where a method is implemented, not where it is declared.
-    #
-    tests = [
-      # Local::x -> declaration of x
-      { 'request': [23, 21], 'response': [ 4,  9] },
-      # Local::in_line -> declaration of Local::in_line
-      { 'request': [24, 26], 'response': [ 6, 10] },
-      # Local -> declaration of Local
-      { 'request': [24, 16], 'response': [ 2, 11] },
-      # sic: Local::out_of_line -> definition of Local::out_of_line
-      { 'request': [25, 27], 'response': [14, 13] }, # sic
-      # sic: GoToDeclaration on definition of out_of_line moves to itself
-      { 'request': [14, 13], 'response': [14, 13] }, # sic
-      # main -> definition of main (not declaration)
-      { 'request': [21,  7], 'response': [21, 5] }, # sic
-    ]
-
-    for test in tests:
-      yield ( self._GoTo_all,
-              'GoTo_all_Clang_test.cc',
-              [ 'GoToImprecise' ],
-              test )
-
-
-  def _GoToInclude( self, command, test ):
-    self._app.post_json(
-      '/load_extra_conf_file',
-      { 'filepath': self._PathToTestFile( 'test-include',
-                                          '.ycm_extra_conf.py' ) } )
-
-    filepath = self._PathToTestFile( 'test-include', 'main.cpp' )
-    goto_data = self._BuildRequest( filepath = filepath,
-                                    filetype = 'cpp',
-                                    contents = ReadFile( filepath ),
-                                    command_arguments = [ command ],
-                                    line_num = test[ 'request' ][ 0 ],
-                                    column_num = test[ 'request' ][ 1 ] )
-
-    response = {
-      'filepath'   : self._PathToTestFile( 'test-include', test[ 'response' ] ),
-      'line_num'   : 1,
-      'column_num' : 1,
-    }
-
-    eq_( response,
-         self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoToInclude_test( self ):
-    tests = [
-      { 'request': [ 1, 1 ], 'response': 'a.hpp' },
-      { 'request': [ 2, 1 ], 'response': os.path.join( 'system', 'a.hpp' ) },
-      { 'request': [ 3, 1 ], 'response': os.path.join( 'quote',  'b.hpp' ) },
-      { 'request': [ 5, 1 ], 'response': os.path.join( 'system', 'c.hpp' ) },
-      { 'request': [ 6, 1 ], 'response': os.path.join( 'system', 'c.hpp' ) },
-    ]
-    for test in tests:
-      yield self._GoToInclude, 'GoToInclude', test
-      yield self._GoToInclude, 'GoTo', test
-      yield self._GoToInclude, 'GoToImprecise', test
-
-
-  def GoToInclude_Fail_test( self ):
-    test = { 'request': [ 4, 1 ], 'response': '' }
-    assert_that(
-      calling( self._GoToInclude ).with_args( 'GoToInclude', test ),
-      raises( AppError, 'Include file not found.' ) )
-    assert_that(
-      calling( self._GoToInclude ).with_args( 'GoTo', test ),
-      raises( AppError, 'Include file not found.' ) )
-    assert_that(
-      calling( self._GoToInclude ).with_args( 'GoToImprecise', test ),
-      raises( AppError, 'Include file not found.' ) )
-
-    test = { 'request': [ 7, 1 ], 'response': '' }
-    assert_that(
-      calling( self._GoToInclude ).with_args( 'GoToInclude', test ),
-      raises( AppError, 'Not an include/import line.' ) )
-    assert_that(
-      calling( self._GoToInclude ).with_args( 'GoTo', test ),
-      raises( AppError, r'Can\\\'t jump to definition or declaration.' ) )
-    assert_that(
-      calling( self._GoToInclude ).with_args( 'GoToImprecise', test ),
-      raises( AppError, r'Can\\\'t jump to definition or declaration.' ) )
-
-
-  def _Message( self, filename, test, command):
-    contents = ReadFile( self._PathToTestFile( filename ) )
-
-    # We use the -fno-delayed-template-parsing flag to not delay
-    # parsing of templates on Windows.  This is the default on
-    # other platforms.  See the _ExtraClangFlags function in
-    # ycmd/completers/cpp/flags.py file for more information.
-    common_args = {
-      'completer_target' : 'filetype_default',
-      'command_arguments': command,
+
+@SharedYcmd
+def Subcommands_GoTo_ZeroBasedLineAndColumn_test( app ):
+  contents = ReadFile( PathToTestFile(
+    'GoTo_Clang_ZeroBasedLineAndColumn_test.cc' ) )
+
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = ['GoToDefinition'],
+                            compilation_flags = ['-x', 'c++'],
+                            line_num = 10,
+                            column_num = 3,
+                            contents = contents,
+                            filetype = 'cpp' )
+
+  eq_( {
+    'filepath': os.path.abspath( '/foo' ),
+    'line_num': 2,
+    'column_num': 8
+  }, app.post_json( '/run_completer_command', goto_data ).json )
+
+
+@SharedYcmd
+def RunGoToTest_all( app, filename, command, test ):
+  contents = ReadFile( PathToTestFile( filename ) )
+  common_request = {
+    'completer_target' : 'filetype_default',
+    'command_arguments': command,
+    'compilation_flags': ['-x',
+                          'c++'],
+    'line_num'         : 10,
+    'column_num'       : 3,
+    'contents'         : contents,
+    'filetype'         : 'cpp'
+  }
+  common_response = {
+    'filepath': os.path.abspath( '/foo' ),
+  }
+
+  request = common_request
+  request.update( {
+      'line_num'  : test['request'][0],
+      'column_num': test['request'][1],
+  })
+  response = common_response
+  response.update({
+      'line_num'  : test['response'][0],
+      'column_num': test['response'][1],
+  })
+
+  goto_data = BuildRequest( **request )
+
+  eq_( response,
+       app.post_json( '/run_completer_command', goto_data ).json )
+
+
+def Subcommands_GoTo_all_test():
+  # GoToDeclaration
+  tests = [
+    # Local::x -> declaration of x
+    { 'request': [23, 21], 'response': [ 4,  9] },
+    # Local::in_line -> declaration of Local::in_line
+    { 'request': [24, 26], 'response': [ 6, 10] },
+    # Local -> declaration of Local
+    { 'request': [24, 16], 'response': [ 2, 11] },
+    # Local::out_of_line -> declaration of Local::out_of_line
+    { 'request': [25, 27], 'response': [11, 10] },
+    # GoToDeclaration on definition of out_of_line moves to declaration
+    { 'request': [14, 13], 'response': [11, 10] },
+    # main -> declaration of main
+    { 'request': [21,  7], 'response': [19, 5] },
+  ]
+
+  for test in tests:
+    yield ( RunGoToTest_all,
+            'GoTo_all_Clang_test.cc',
+            [ 'GoToDeclaration' ],
+            test )
+
+  # GoToDefinition - identical to GoToDeclaration
+  #
+  # The semantics of this seem the wrong way round to me. GoToDefinition should
+  # go to where a method is implemented, not where it is declared.
+  #
+  tests = [
+    # Local::x -> declaration of x
+    { 'request': [23, 21], 'response': [ 4,  9] },
+    # Local::in_line -> declaration of Local::in_line
+    { 'request': [24, 26], 'response': [ 6, 10] },
+    # Local -> declaration of Local
+    { 'request': [24, 16], 'response': [ 2, 11] },
+    # sic: Local::out_of_line -> definition of Local::out_of_line
+    { 'request': [25, 27], 'response': [14, 13] }, # sic
+    # sic: GoToDeclaration on definition of out_of_line moves to itself
+    { 'request': [14, 13], 'response': [14, 13] }, # sic
+    # main -> definition of main (not declaration)
+    { 'request': [21,  7], 'response': [21, 5] }, # sic
+  ]
+
+  for test in tests:
+    yield ( RunGoToTest_all,
+            'GoTo_all_Clang_test.cc',
+            [ 'GoToDefinition' ],
+            test )
+
+  # GoTo - identical to GoToDeclaration
+  #
+  # The semantics of this seem the wrong way round to me. GoToDefinition should
+  # go to where a method is implemented, not where it is declared.
+  #
+  tests = [
+    # Local::x -> declaration of x
+    { 'request': [23, 21], 'response': [ 4,  9] },
+    # Local::in_line -> declaration of Local::in_line
+    { 'request': [24, 26], 'response': [ 6, 10] },
+    # Local -> declaration of Local
+    { 'request': [24, 16], 'response': [ 2, 11] },
+    # sic: Local::out_of_line -> definition of Local::out_of_line
+    { 'request': [25, 27], 'response': [14, 13] }, # sic
+    # sic: GoToDeclaration on definition of out_of_line moves to itself
+    { 'request': [14, 13], 'response': [14, 13] }, # sic
+    # main -> definition of main (not declaration)
+    { 'request': [21,  7], 'response': [21, 5] }, # sic
+  ]
+
+  for test in tests:
+    yield ( RunGoToTest_all,
+            'GoTo_all_Clang_test.cc',
+            [ 'GoTo' ],
+            test )
+
+  # GoToImprecise - identical to GoToDeclaration
+  #
+  # The semantics of this seem the wrong way round to me. GoToDefinition should
+  # go to where a method is implemented, not where it is declared.
+  #
+  tests = [
+    # Local::x -> declaration of x
+    { 'request': [23, 21], 'response': [ 4,  9] },
+    # Local::in_line -> declaration of Local::in_line
+    { 'request': [24, 26], 'response': [ 6, 10] },
+    # Local -> declaration of Local
+    { 'request': [24, 16], 'response': [ 2, 11] },
+    # sic: Local::out_of_line -> definition of Local::out_of_line
+    { 'request': [25, 27], 'response': [14, 13] }, # sic
+    # sic: GoToDeclaration on definition of out_of_line moves to itself
+    { 'request': [14, 13], 'response': [14, 13] }, # sic
+    # main -> definition of main (not declaration)
+    { 'request': [21,  7], 'response': [21, 5] }, # sic
+  ]
+
+  for test in tests:
+    yield ( RunGoToTest_all,
+            'GoTo_all_Clang_test.cc',
+            [ 'GoToImprecise' ],
+            test )
+
+
+@SharedYcmd
+def RunGoToIncludeTest( app, command, test ):
+  app.post_json(
+    '/load_extra_conf_file',
+    { 'filepath': PathToTestFile( 'test-include',
+                                  '.ycm_extra_conf.py' ) } )
+
+  filepath = PathToTestFile( 'test-include', 'main.cpp' )
+  goto_data = BuildRequest( filepath = filepath,
+                            filetype = 'cpp',
+                            contents = ReadFile( filepath ),
+                            command_arguments = [ command ],
+                            line_num = test[ 'request' ][ 0 ],
+                            column_num = test[ 'request' ][ 1 ] )
+
+  response = {
+    'filepath'   : PathToTestFile( 'test-include', test[ 'response' ] ),
+    'line_num'   : 1,
+    'column_num' : 1,
+  }
+
+  eq_( response,
+       app.post_json( '/run_completer_command', goto_data ).json )
+
+
+def Subcommands_GoToInclude_test():
+  tests = [
+    { 'request': [ 1, 1 ], 'response': 'a.hpp' },
+    { 'request': [ 2, 1 ], 'response': os.path.join( 'system', 'a.hpp' ) },
+    { 'request': [ 3, 1 ], 'response': os.path.join( 'quote',  'b.hpp' ) },
+    { 'request': [ 5, 1 ], 'response': os.path.join( 'system', 'c.hpp' ) },
+    { 'request': [ 6, 1 ], 'response': os.path.join( 'system', 'c.hpp' ) },
+  ]
+  for test in tests:
+    yield RunGoToIncludeTest, 'GoToInclude', test
+    yield RunGoToIncludeTest, 'GoTo', test
+    yield RunGoToIncludeTest, 'GoToImprecise', test
+
+
+def Subcommands_GoToInclude_Fail_test():
+  test = { 'request': [ 4, 1 ], 'response': '' }
+  assert_that(
+    calling( RunGoToIncludeTest ).with_args( 'GoToInclude', test ),
+    raises( AppError, 'Include file not found.' ) )
+  assert_that(
+    calling( RunGoToIncludeTest ).with_args( 'GoTo', test ),
+    raises( AppError, 'Include file not found.' ) )
+  assert_that(
+    calling( RunGoToIncludeTest ).with_args( 'GoToImprecise', test ),
+    raises( AppError, 'Include file not found.' ) )
+
+  test = { 'request': [ 7, 1 ], 'response': '' }
+  assert_that(
+    calling( RunGoToIncludeTest ).with_args( 'GoToInclude', test ),
+    raises( AppError, 'Not an include/import line.' ) )
+  assert_that(
+    calling( RunGoToIncludeTest ).with_args( 'GoTo', test ),
+    raises( AppError, r'Can\\\'t jump to definition or declaration.' ) )
+  assert_that(
+    calling( RunGoToIncludeTest ).with_args( 'GoToImprecise', test ),
+    raises( AppError, r'Can\\\'t jump to definition or declaration.' ) )
+
+
+@SharedYcmd
+def RunGetSemanticTest( app, filename, test, command):
+  contents = ReadFile( PathToTestFile( filename ) )
+
+  # We use the -fno-delayed-template-parsing flag to not delay
+  # parsing of templates on Windows.  This is the default on
+  # other platforms.  See the _ExtraClangFlags function in
+  # ycmd/completers/cpp/flags.py file for more information.
+  common_args = {
+    'completer_target' : 'filetype_default',
+    'command_arguments': command,
+    'compilation_flags': [ '-x',
+                           'c++',
+                           # C++11 flag is needed for lambda functions
+                           '-std=c++11',
+                           '-fno-delayed-template-parsing' ],
+    'line_num'         : 10,
+    'column_num'       : 3,
+    'contents'         : contents,
+    'filetype'         : 'cpp'
+  }
+
+  args = test[ 0 ]
+  expected = test[ 1 ]
+
+  request = common_args
+  request.update( args )
+
+  request_data = BuildRequest( **request )
+
+  eq_( { 'message': expected },
+       app.post_json( '/run_completer_command', request_data ).json )
+
+
+def Subcommands_GetType_test():
+  tests = [
+    # Basic pod types
+    [{'line_num': 20, 'column_num':  3}, 'Foo'],
+    [{'line_num':  1, 'column_num':  1}, 'Internal error: cursor not valid'],
+    [{'line_num': 12, 'column_num':  2}, 'Foo'],
+    [{'line_num': 12, 'column_num':  8}, 'Foo'],
+    [{'line_num': 12, 'column_num':  9}, 'Foo'],
+    [{'line_num': 12, 'column_num': 10}, 'Foo'],
+    [{'line_num': 13, 'column_num':  3}, 'int'],
+    [{'line_num': 13, 'column_num':  7}, 'int'],
+    [{'line_num': 15, 'column_num':  7}, 'char'],
+
+    # Function
+    [{'line_num': 18, 'column_num':  2}, 'int ()'],
+    [{'line_num': 18, 'column_num':  6}, 'int ()'],
+
+    # Declared and canonical type
+    # On Ns:: (Unknown)
+    [{'line_num': 21, 'column_num':  3}, 'Unknown type'], # sic
+    # On Type (Type)
+    [{'line_num': 21, 'column_num':  8}, 'Type => Ns::BasicType<char>'], # sic
+    # On "a" (Ns::Type)
+    [{'line_num': 21, 'column_num': 15}, 'Ns::Type => Ns::BasicType<char>'],
+    [{'line_num': 22, 'column_num': 13}, 'Ns::Type => Ns::BasicType<char>'],
+
+    # Cursor on decl for refs & pointers
+    [{'line_num': 35, 'column_num':  3}, 'Foo'],
+    [{'line_num': 35, 'column_num': 11}, 'Foo &'],
+    [{'line_num': 35, 'column_num': 15}, 'Foo'],
+    [{'line_num': 36, 'column_num':  3}, 'Foo'],
+    [{'line_num': 36, 'column_num': 11}, 'Foo *'],
+    [{'line_num': 36, 'column_num': 18}, 'Foo'],
+    [{'line_num': 38, 'column_num':  3}, 'const Foo &'],
+    [{'line_num': 38, 'column_num': 16}, 'const Foo &'],
+    [{'line_num': 39, 'column_num':  3}, 'const Foo *'],
+    [{'line_num': 39, 'column_num': 16}, 'const Foo *'],
+
+    # Cursor on usage
+    [{'line_num': 41, 'column_num': 13}, 'const Foo'],
+    [{'line_num': 41, 'column_num': 19}, 'const int'],
+    [{'line_num': 42, 'column_num': 13}, 'const Foo *'],
+    [{'line_num': 42, 'column_num': 20}, 'const int'],
+    [{'line_num': 43, 'column_num': 12}, 'Foo'],
+    [{'line_num': 43, 'column_num': 17}, 'int'],
+    [{'line_num': 44, 'column_num': 12}, 'Foo *'],
+    [{'line_num': 44, 'column_num': 18}, 'int'],
+
+    # Auto behaves strangely (bug in libclang)
+    [{'line_num': 24, 'column_num':  3}, 'auto &'], # sic
+    [{'line_num': 24, 'column_num': 11}, 'auto &'], # sic
+    [{'line_num': 24, 'column_num': 18}, 'Foo'],
+    [{'line_num': 25, 'column_num':  3}, 'auto *'], # sic
+    [{'line_num': 25, 'column_num': 11}, 'auto *'], # sic
+    [{'line_num': 25, 'column_num': 18}, 'Foo'],
+    [{'line_num': 27, 'column_num':  3}, 'const auto &'], # sic
+    [{'line_num': 27, 'column_num': 16}, 'const auto &'], # sic
+    [{'line_num': 28, 'column_num':  3}, 'const auto *'], # sic
+    [{'line_num': 28, 'column_num': 16}, 'const auto *'], # sic
+
+    # Auto sort of works in usage (but canonical types apparently differ)
+    [{'line_num': 30, 'column_num': 14}, 'const Foo => const Foo'], # sic
+    [{'line_num': 30, 'column_num': 21}, 'const int'],
+    [{'line_num': 31, 'column_num': 14}, 'const Foo * => const Foo *'], # sic
+    [{'line_num': 31, 'column_num': 22}, 'const int'],
+    [{'line_num': 32, 'column_num': 13}, 'Foo => Foo'], # sic
+    [{'line_num': 32, 'column_num': 19}, 'int'],
+    [{'line_num': 33, 'column_num': 13}, 'Foo * => Foo *'], # sic
+    [{'line_num': 33, 'column_num': 20}, 'int'],
+  ]
+
+  for test in tests:
+    yield ( RunGetSemanticTest,
+            'GetType_Clang_test.cc',
+            test,
+            [ 'GetType' ] )
+
+
+def Subcommands_GetParent_test():
+  tests = [
+    [{'line_num':  1,  'column_num':  1}, 'Internal error: cursor not valid'],
+    # Would be file name if we had one:
+    [{'line_num':  2,  'column_num':  8}, '/foo'],
+
+    # The reported scope does not include parents
+    [{'line_num':  3,  'column_num': 11}, 'A'],
+    [{'line_num':  4,  'column_num': 13}, 'B'],
+    [{'line_num':  5,  'column_num': 13}, 'B'],
+    [{'line_num':  9,  'column_num': 17}, 'do_z_inline()'],
+    [{'line_num': 15,  'column_num': 22}, 'do_anything(T &)'],
+    [{'line_num': 19,  'column_num':  9}, 'A'],
+    [{'line_num': 20,  'column_num':  9}, 'A'],
+    [{'line_num': 22,  'column_num': 12}, 'A'],
+    [{'line_num': 23,  'column_num':  5}, 'do_Z_inline()'],
+    [{'line_num': 24,  'column_num': 12}, 'do_Z_inline()'],
+    [{'line_num': 28,  'column_num': 14}, 'A'],
+
+    [{'line_num': 34,  'column_num':  1}, 'do_anything(T &)'],
+    [{'line_num': 39,  'column_num':  1}, 'do_x()'],
+    [{'line_num': 44,  'column_num':  1}, 'do_y()'],
+    [{'line_num': 49,  'column_num':  1}, 'main()'],
+
+    # Lambdas report the name of the variable
+    [{'line_num': 49,  'column_num': 14}, 'l'],
+    [{'line_num': 50,  'column_num': 19}, 'l'],
+    [{'line_num': 51,  'column_num': 16}, 'main()'],
+  ]
+
+  for test in tests:
+    yield ( RunGetSemanticTest,
+            'GetParent_Clang_test.cc',
+            test,
+            [ 'GetParent' ] )
+
+
+@SharedYcmd
+def RunFixItTest( app, line, column, lang, file_name, check ):
+  contents = ReadFile( PathToTestFile( file_name ) )
+
+  language_options = {
+    'cpp11': {
       'compilation_flags': [ '-x',
                              'c++',
-                             # C++11 flag is needed for lambda functions
                              '-std=c++11',
-                             '-fno-delayed-template-parsing' ],
-      'line_num'         : 10,
-      'column_num'       : 3,
-      'contents'         : contents,
-      'filetype'         : 'cpp'
-    }
-
-    args = test[ 0 ]
-    expected = test[ 1 ]
-
-    request = common_args
-    request.update( args )
-
-    request_data = self._BuildRequest( **request )
-
-    eq_( { 'message': expected },
-         self._app.post_json( '/run_completer_command', request_data ).json )
-
-
-  def GetType_test( self ):
-    tests = [
-      # Basic pod types
-      [{'line_num': 20, 'column_num':  3}, 'Foo'],
-      [{'line_num':  1, 'column_num':  1}, 'Internal error: cursor not valid'],
-      [{'line_num': 12, 'column_num':  2}, 'Foo'],
-      [{'line_num': 12, 'column_num':  8}, 'Foo'],
-      [{'line_num': 12, 'column_num':  9}, 'Foo'],
-      [{'line_num': 12, 'column_num': 10}, 'Foo'],
-      [{'line_num': 13, 'column_num':  3}, 'int'],
-      [{'line_num': 13, 'column_num':  7}, 'int'],
-      [{'line_num': 15, 'column_num':  7}, 'char'],
-
-      # Function
-      [{'line_num': 18, 'column_num':  2}, 'int ()'],
-      [{'line_num': 18, 'column_num':  6}, 'int ()'],
-
-      # Declared and canonical type
-      # On Ns:: (Unknown)
-      [{'line_num': 21, 'column_num':  3}, 'Unknown type'], # sic
-      # On Type (Type)
-      [{'line_num': 21, 'column_num':  8}, 'Type => Ns::BasicType<char>'], # sic
-      # On "a" (Ns::Type)
-      [{'line_num': 21, 'column_num': 15}, 'Ns::Type => Ns::BasicType<char>'],
-      [{'line_num': 22, 'column_num': 13}, 'Ns::Type => Ns::BasicType<char>'],
-
-      # Cursor on decl for refs & pointers
-      [{'line_num': 35, 'column_num':  3}, 'Foo'],
-      [{'line_num': 35, 'column_num': 11}, 'Foo &'],
-      [{'line_num': 35, 'column_num': 15}, 'Foo'],
-      [{'line_num': 36, 'column_num':  3}, 'Foo'],
-      [{'line_num': 36, 'column_num': 11}, 'Foo *'],
-      [{'line_num': 36, 'column_num': 18}, 'Foo'],
-      [{'line_num': 38, 'column_num':  3}, 'const Foo &'],
-      [{'line_num': 38, 'column_num': 16}, 'const Foo &'],
-      [{'line_num': 39, 'column_num':  3}, 'const Foo *'],
-      [{'line_num': 39, 'column_num': 16}, 'const Foo *'],
-
-      # Cursor on usage
-      [{'line_num': 41, 'column_num': 13}, 'const Foo'],
-      [{'line_num': 41, 'column_num': 19}, 'const int'],
-      [{'line_num': 42, 'column_num': 13}, 'const Foo *'],
-      [{'line_num': 42, 'column_num': 20}, 'const int'],
-      [{'line_num': 43, 'column_num': 12}, 'Foo'],
-      [{'line_num': 43, 'column_num': 17}, 'int'],
-      [{'line_num': 44, 'column_num': 12}, 'Foo *'],
-      [{'line_num': 44, 'column_num': 18}, 'int'],
-
-      # Auto behaves strangely (bug in libclang)
-      [{'line_num': 24, 'column_num':  3}, 'auto &'], # sic
-      [{'line_num': 24, 'column_num': 11}, 'auto &'], # sic
-      [{'line_num': 24, 'column_num': 18}, 'Foo'],
-      [{'line_num': 25, 'column_num':  3}, 'auto *'], # sic
-      [{'line_num': 25, 'column_num': 11}, 'auto *'], # sic
-      [{'line_num': 25, 'column_num': 18}, 'Foo'],
-      [{'line_num': 27, 'column_num':  3}, 'const auto &'], # sic
-      [{'line_num': 27, 'column_num': 16}, 'const auto &'], # sic
-      [{'line_num': 28, 'column_num':  3}, 'const auto *'], # sic
-      [{'line_num': 28, 'column_num': 16}, 'const auto *'], # sic
-
-      # Auto sort of works in usage (but canonical types apparently differ)
-      [{'line_num': 30, 'column_num': 14}, 'const Foo => const Foo'], # sic
-      [{'line_num': 30, 'column_num': 21}, 'const int'],
-      [{'line_num': 31, 'column_num': 14}, 'const Foo * => const Foo *'], # sic
-      [{'line_num': 31, 'column_num': 22}, 'const int'],
-      [{'line_num': 32, 'column_num': 13}, 'Foo => Foo'], # sic
-      [{'line_num': 32, 'column_num': 19}, 'int'],
-      [{'line_num': 33, 'column_num': 13}, 'Foo * => Foo *'], # sic
-      [{'line_num': 33, 'column_num': 20}, 'int'],
-    ]
-
-    for test in tests:
-      yield ( self._Message,
-              'GetType_Clang_test.cc',
-              test,
-              [ 'GetType' ] )
-
-
-  def GetParent_test( self ):
-    tests = [
-      [{'line_num':  1,  'column_num':  1}, 'Internal error: cursor not valid'],
-      # Would be file name if we had one:
-      [{'line_num':  2,  'column_num':  8}, '/foo'],
-
-      # The reported scope does not include parents
-      [{'line_num':  3,  'column_num': 11}, 'A'],
-      [{'line_num':  4,  'column_num': 13}, 'B'],
-      [{'line_num':  5,  'column_num': 13}, 'B'],
-      [{'line_num':  9,  'column_num': 17}, 'do_z_inline()'],
-      [{'line_num': 15,  'column_num': 22}, 'do_anything(T &)'],
-      [{'line_num': 19,  'column_num':  9}, 'A'],
-      [{'line_num': 20,  'column_num':  9}, 'A'],
-      [{'line_num': 22,  'column_num': 12}, 'A'],
-      [{'line_num': 23,  'column_num':  5}, 'do_Z_inline()'],
-      [{'line_num': 24,  'column_num': 12}, 'do_Z_inline()'],
-      [{'line_num': 28,  'column_num': 14}, 'A'],
-
-      [{'line_num': 34,  'column_num':  1}, 'do_anything(T &)'],
-      [{'line_num': 39,  'column_num':  1}, 'do_x()'],
-      [{'line_num': 44,  'column_num':  1}, 'do_y()'],
-      [{'line_num': 49,  'column_num':  1}, 'main()'],
-
-      # Lambdas report the name of the variable
-      [{'line_num': 49,  'column_num': 14}, 'l'],
-      [{'line_num': 50,  'column_num': 19}, 'l'],
-      [{'line_num': 51,  'column_num': 16}, 'main()'],
-    ]
-
-    for test in tests:
-      yield ( self._Message,
-              'GetParent_Clang_test.cc',
-              test,
-              [ 'GetParent' ] )
-
-
-  def _RunFixIt( self, line, column, lang, file_name, check ):
-    contents = ReadFile( self._PathToTestFile( file_name ) )
-
-    language_options = {
-      'cpp11': {
-        'compilation_flags': [ '-x',
-                                'c++',
-                                '-std=c++11',
-                                '-Wall',
-                                '-Wextra',
-                                '-pedantic' ],
-        'filetype'         : 'cpp',
-      },
-      'objective-c': {
-        'compilation_flags': [ '-x',
-                               'objective-c',
-                               '-Wall',
-                               '-Wextra' ],
-        'filetype'         : 'objc',
-      },
-    }
-
-    # Build the command arguments from the standard ones and the
-    # language-specific arguments.
-    args = {
-      'completer_target' : 'filetype_default',
-      'contents'         : contents,
-      'command_arguments': [ 'FixIt' ],
-      'line_num'         : line,
-      'column_num'       : column,
-    }
-    args.update( language_options[ lang ] )
-
-    # get the diagnostics for the file
-    event_data = self._BuildRequest( **args )
-
-    results = self._app.post_json( '/run_completer_command', event_data ).json
-
-    pprint( results )
-    check( results )
-
-
-  def _FixIt_Check_cpp11_Ins( self, results ):
-    # First fixit
-    #   switch(A()) { // expected-error{{explicit conversion to}}
-    assert_that( results, has_entries( {
-      'fixits': contains( has_entries( {
-        'chunks': contains(
-          has_entries( {
-            'replacement_text': equal_to('static_cast<int>('),
-            'range': has_entries( {
-              'start': has_entries( { 'line_num': 16, 'column_num': 10 } ),
-              'end'  : has_entries( { 'line_num': 16, 'column_num': 10 } ),
-            } ),
+                             '-Wall',
+                             '-Wextra',
+                             '-pedantic' ],
+      'filetype'         : 'cpp',
+    },
+    'objective-c': {
+      'compilation_flags': [ '-x',
+                             'objective-c',
+                             '-Wall',
+                             '-Wextra' ],
+      'filetype'         : 'objc',
+    },
+  }
+
+  # Build the command arguments from the standard ones and the language-specific
+  # arguments.
+  args = {
+    'completer_target' : 'filetype_default',
+    'contents'         : contents,
+    'command_arguments': [ 'FixIt' ],
+    'line_num'         : line,
+    'column_num'       : column,
+  }
+  args.update( language_options[ lang ] )
+
+  # Get the diagnostics for the file.
+  event_data = BuildRequest( **args )
+
+  results = app.post_json( '/run_completer_command', event_data ).json
+
+  pprint( results )
+  check( results )
+
+
+def FixIt_Check_cpp11_Ins( results ):
+  # First fixit
+  #   switch(A()) { // expected-error{{explicit conversion to}}
+  assert_that( results, has_entries( {
+    'fixits': contains( has_entries( {
+      'chunks': contains(
+        has_entries( {
+          'replacement_text': equal_to('static_cast<int>('),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 16, 'column_num': 10 } ),
+            'end'  : has_entries( { 'line_num': 16, 'column_num': 10 } ),
           } ),
-          has_entries( {
-            'replacement_text': equal_to(')'),
-            'range': has_entries( {
-              'start': has_entries( { 'line_num': 16, 'column_num': 13 } ),
-              'end'  : has_entries( { 'line_num': 16, 'column_num': 13 } ),
-            } ),
-          } )
-        ),
-        'location': has_entries( { 'line_num': 16, 'column_num': 3 } )
-      } ) )
+        } ),
+        has_entries( {
+          'replacement_text': equal_to(')'),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 16, 'column_num': 13 } ),
+            'end'  : has_entries( { 'line_num': 16, 'column_num': 13 } ),
+          } ),
+        } )
+      ),
+      'location': has_entries( { 'line_num': 16, 'column_num': 3 } )
     } ) )
+  } ) )
 
 
-  def _FixIt_Check_cpp11_InsMultiLine( self, results ):
-    # Similar to _FixIt_Check_cpp11_1 but inserts split across lines
-    #
-    assert_that( results, has_entries( {
-      'fixits': contains( has_entries( {
-        'chunks': contains(
-          has_entries( {
-            'replacement_text': equal_to('static_cast<int>('),
-            'range': has_entries( {
-              'start': has_entries( { 'line_num': 26, 'column_num': 7 } ),
-              'end'  : has_entries( { 'line_num': 26, 'column_num': 7 } ),
-            } ),
+def FixIt_Check_cpp11_InsMultiLine( results ):
+  # Similar to FixIt_Check_cpp11_1 but inserts split across lines
+  #
+  assert_that( results, has_entries( {
+    'fixits': contains( has_entries( {
+      'chunks': contains(
+        has_entries( {
+          'replacement_text': equal_to('static_cast<int>('),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 26, 'column_num': 7 } ),
+            'end'  : has_entries( { 'line_num': 26, 'column_num': 7 } ),
           } ),
-          has_entries( {
-            'replacement_text': equal_to(')'),
-            'range': has_entries( {
-              'start': has_entries( { 'line_num': 28, 'column_num': 2 } ),
-              'end'  : has_entries( { 'line_num': 28, 'column_num': 2 } ),
-            } ),
-          } )
-        ),
-        'location': has_entries( { 'line_num': 25, 'column_num': 3 } )
-      } ) )
+        } ),
+        has_entries( {
+          'replacement_text': equal_to(')'),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 28, 'column_num': 2 } ),
+            'end'  : has_entries( { 'line_num': 28, 'column_num': 2 } ),
+          } ),
+        } )
+      ),
+      'location': has_entries( { 'line_num': 25, 'column_num': 3 } )
+    } ) )
+  } ) )
+
+
+def FixIt_Check_cpp11_Del( results ):
+  # Removal of ::
+  assert_that( results, has_entries( {
+    'fixits': contains( has_entries( {
+      'chunks': contains(
+        has_entries( {
+          'replacement_text': equal_to(''),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 35, 'column_num': 7 } ),
+            'end'  : has_entries( { 'line_num': 35, 'column_num': 9 } ),
+          } ),
+        } )
+      ),
+      'location': has_entries( { 'line_num': 35, 'column_num': 7 } )
+    } ) )
+  } ) )
+
+
+def FixIt_Check_cpp11_Repl( results ):
+  assert_that( results, has_entries( {
+    'fixits': contains( has_entries( {
+      'chunks': contains(
+        has_entries( {
+          'replacement_text': equal_to('foo'),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 40, 'column_num': 6 } ),
+            'end'  : has_entries( { 'line_num': 40, 'column_num': 9 } ),
+          } ),
+        } )
+      ),
+      'location': has_entries( { 'line_num': 40, 'column_num': 6 } )
+    } ) )
+  } ) )
+
+
+def FixIt_Check_cpp11_DelAdd( results ):
+  assert_that( results, has_entries( {
+    'fixits': contains( has_entries( {
+      'chunks': contains(
+        has_entries( {
+          'replacement_text': equal_to(''),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 48, 'column_num': 3 } ),
+            'end'  : has_entries( { 'line_num': 48, 'column_num': 4 } ),
+          } ),
+        } ),
+        has_entries( {
+          'replacement_text': equal_to('~'),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 48, 'column_num': 9 } ),
+            'end'  : has_entries( { 'line_num': 48, 'column_num': 9 } ),
+          } ),
+        } ),
+      ),
+      'location': has_entries( { 'line_num': 48, 'column_num': 3 } )
+    } ) )
+  } ) )
+
+
+def FixIt_Check_objc( results ):
+  assert_that( results, has_entries( {
+    'fixits': contains( has_entries( {
+      'chunks': contains(
+        has_entries( {
+          'replacement_text': equal_to('id'),
+          'range': has_entries( {
+            'start': has_entries( { 'line_num': 5, 'column_num': 3 } ),
+            'end'  : has_entries( { 'line_num': 5, 'column_num': 3 } ),
+          } ),
+        } )
+      ),
+      'location': has_entries( { 'line_num': 5, 'column_num': 3 } )
     } ) )
+  } ) )
+
+
+def FixIt_Check_objc_NoFixIt( results ):
+  # and finally, a warning with no fixits
+  assert_that( results, equal_to( { 'fixits': [] } ) )
 
 
-  def _FixIt_Check_cpp11_Del( self, results ):
-    # Removal of ::
-    assert_that( results, has_entries( {
-      'fixits': contains( has_entries( {
+def FixIt_Check_cpp11_MultiFirst( results ):
+  assert_that( results, has_entries( {
+    'fixits': contains(
+      # first fix-it at 54,16
+      has_entries( {
         'chunks': contains(
           has_entries( {
-            'replacement_text': equal_to(''),
+            'replacement_text': equal_to('foo'),
             'range': has_entries( {
-              'start': has_entries( { 'line_num': 35, 'column_num': 7 } ),
-              'end'  : has_entries( { 'line_num': 35, 'column_num': 9 } ),
+              'start': has_entries( { 'line_num': 54, 'column_num': 16 } ),
+              'end'  : has_entries( { 'line_num': 54, 'column_num': 19 } ),
             } ),
           } )
         ),
-        'location': has_entries( { 'line_num': 35, 'column_num': 7 } )
-      } ) )
-    } ) )
-
-
-  def _FixIt_Check_cpp11_Repl( self, results ):
-    assert_that( results, has_entries( {
-      'fixits': contains( has_entries( {
+        'location': has_entries( { 'line_num': 54, 'column_num': 16 } )
+      } ),
+      # second fix-it at 54,52
+      has_entries( {
         'chunks': contains(
           has_entries( {
-            'replacement_text': equal_to('foo'),
+            'replacement_text': equal_to(''),
             'range': has_entries( {
-              'start': has_entries( { 'line_num': 40, 'column_num': 6 } ),
-              'end'  : has_entries( { 'line_num': 40, 'column_num': 9 } ),
+              'start': has_entries( { 'line_num': 54, 'column_num': 52 } ),
+              'end'  : has_entries( { 'line_num': 54, 'column_num': 53 } ),
             } ),
-          } )
+          } ),
+          has_entries( {
+            'replacement_text': equal_to('~'),
+            'range': has_entries( {
+              'start': has_entries( { 'line_num': 54, 'column_num': 58 } ),
+              'end'  : has_entries( { 'line_num': 54, 'column_num': 58 } ),
+            } ),
+          } ),
         ),
-        'location': has_entries( { 'line_num': 40, 'column_num': 6 } )
-      } ) )
-    } ) )
+        'location': has_entries( { 'line_num': 54, 'column_num': 52 } )
+      } )
+    )
+  } ) )
 
 
-  def _FixIt_Check_cpp11_DelAdd( self, results ):
-    assert_that( results, has_entries( {
-      'fixits': contains( has_entries( {
+def FixIt_Check_cpp11_MultiSecond( results ):
+  assert_that( results, has_entries( {
+    'fixits': contains(
+      # second fix-it at 54,52
+      has_entries( {
         'chunks': contains(
           has_entries( {
             'replacement_text': equal_to(''),
             'range': has_entries( {
-              'start': has_entries( { 'line_num': 48, 'column_num': 3 } ),
-              'end'  : has_entries( { 'line_num': 48, 'column_num': 4 } ),
+              'start': has_entries( { 'line_num': 54, 'column_num': 52 } ),
+              'end'  : has_entries( { 'line_num': 54, 'column_num': 53 } ),
             } ),
           } ),
           has_entries( {
             'replacement_text': equal_to('~'),
             'range': has_entries( {
-              'start': has_entries( { 'line_num': 48, 'column_num': 9 } ),
-              'end'  : has_entries( { 'line_num': 48, 'column_num': 9 } ),
+              'start': has_entries( { 'line_num': 54, 'column_num': 58 } ),
+              'end'  : has_entries( { 'line_num': 54, 'column_num': 58 } ),
             } ),
           } ),
         ),
-        'location': has_entries( { 'line_num': 48, 'column_num': 3 } )
-      } ) )
-    } ) )
-
-
-  def _FixIt_Check_objc( self, results ):
-    assert_that( results, has_entries( {
-      'fixits': contains( has_entries( {
+        'location': has_entries( { 'line_num': 54, 'column_num': 52 } )
+      } ),
+      # first fix-it at 54,16
+      has_entries( {
         'chunks': contains(
           has_entries( {
-            'replacement_text': equal_to('id'),
+            'replacement_text': equal_to('foo'),
             'range': has_entries( {
-              'start': has_entries( { 'line_num': 5, 'column_num': 3 } ),
-              'end'  : has_entries( { 'line_num': 5, 'column_num': 3 } ),
+              'start': has_entries( { 'line_num': 54, 'column_num': 16 } ),
+              'end'  : has_entries( { 'line_num': 54, 'column_num': 19 } ),
             } ),
           } )
         ),
-        'location': has_entries( { 'line_num': 5, 'column_num': 3 } )
-      } ) )
-    } ) )
-
-
-  def _FixIt_Check_objc_NoFixIt( self, results ):
-    # and finally, a warning with no fixits
-    assert_that( results, equal_to( { 'fixits': [] } ) )
-
-
-  def _FixIt_Check_cpp11_MultiFirst( self, results ):
-    assert_that( results, has_entries( {
-      'fixits': contains(
-        # first fix-it at 54,16
-        has_entries( {
-          'chunks': contains(
-            has_entries( {
-              'replacement_text': equal_to('foo'),
-              'range': has_entries( {
-                'start': has_entries( { 'line_num': 54, 'column_num': 16 } ),
-                'end'  : has_entries( { 'line_num': 54, 'column_num': 19 } ),
-              } ),
-            } )
-          ),
-          'location': has_entries( { 'line_num': 54, 'column_num': 16 } )
-        } ),
-        # second fix-it at 54,52
-        has_entries( {
-          'chunks': contains(
-            has_entries( {
-              'replacement_text': equal_to(''),
-              'range': has_entries( {
-                'start': has_entries( { 'line_num': 54, 'column_num': 52 } ),
-                'end'  : has_entries( { 'line_num': 54, 'column_num': 53 } ),
-              } ),
-            } ),
-            has_entries( {
-              'replacement_text': equal_to('~'),
-              'range': has_entries( {
-                'start': has_entries( { 'line_num': 54, 'column_num': 58 } ),
-                'end'  : has_entries( { 'line_num': 54, 'column_num': 58 } ),
-              } ),
-            } ),
-          ),
-          'location': has_entries( { 'line_num': 54, 'column_num': 52 } )
-        } )
-      )
-    } ) )
-
-
-  def _FixIt_Check_cpp11_MultiSecond( self, results ):
-    assert_that( results, has_entries( {
-      'fixits': contains(
-        # second fix-it at 54,52
-        has_entries( {
-          'chunks': contains(
-            has_entries( {
-              'replacement_text': equal_to(''),
-              'range': has_entries( {
-                'start': has_entries( { 'line_num': 54, 'column_num': 52 } ),
-                'end'  : has_entries( { 'line_num': 54, 'column_num': 53 } ),
-              } ),
-            } ),
-            has_entries( {
-              'replacement_text': equal_to('~'),
-              'range': has_entries( {
-                'start': has_entries( { 'line_num': 54, 'column_num': 58 } ),
-                'end'  : has_entries( { 'line_num': 54, 'column_num': 58 } ),
-              } ),
-            } ),
-          ),
-          'location': has_entries( { 'line_num': 54, 'column_num': 52 } )
-        } ),
-        # first fix-it at 54,16
-        has_entries( {
-          'chunks': contains(
-            has_entries( {
-              'replacement_text': equal_to('foo'),
-              'range': has_entries( {
-                'start': has_entries( { 'line_num': 54, 'column_num': 16 } ),
-                'end'  : has_entries( { 'line_num': 54, 'column_num': 19 } ),
-              } ),
-            } )
-          ),
-          'location': has_entries( { 'line_num': 54, 'column_num': 16 } )
-        } )
-      )
-    } ) )
-
-
-  def FixIt_all_test( self ):
-    cfile = 'FixIt_Clang_cpp11.cpp'
-    mfile = 'FixIt_Clang_objc.m'
-
-    tests = [
-      [ 16, 0,  'cpp11', cfile, self._FixIt_Check_cpp11_Ins ],
-      [ 16, 1,  'cpp11', cfile, self._FixIt_Check_cpp11_Ins ],
-      [ 16, 10, 'cpp11', cfile, self._FixIt_Check_cpp11_Ins ],
-      [ 25, 14, 'cpp11', cfile, self._FixIt_Check_cpp11_InsMultiLine ],
-      [ 25, 0,  'cpp11', cfile, self._FixIt_Check_cpp11_InsMultiLine ],
-      [ 35, 7,  'cpp11', cfile, self._FixIt_Check_cpp11_Del ],
-      [ 40, 6,  'cpp11', cfile, self._FixIt_Check_cpp11_Repl ],
-      [ 48, 3,  'cpp11', cfile, self._FixIt_Check_cpp11_DelAdd ],
-
-      [ 5, 3,   'objective-c', mfile, self._FixIt_Check_objc ],
-      [ 7, 1,   'objective-c', mfile, self._FixIt_Check_objc_NoFixIt ],
-
-      # multiple errors on a single line; both with fixits
-      [ 54, 15, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiFirst ],
-      [ 54, 16, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiFirst ],
-      [ 54, 16, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiFirst ],
-      [ 54, 17, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiFirst ],
-      [ 54, 18, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiFirst ],
-
-      # should put closest fix-it first?
-      [ 54, 51, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiSecond ],
-      [ 54, 52, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiSecond ],
-      [ 54, 53, 'cpp11', cfile, self._FixIt_Check_cpp11_MultiSecond ],
-    ]
-
-    for test in tests:
-      yield self._RunFixIt, test[0], test[1], test[2], test[3], test[4]
-
-
-  def GetDoc_Variable_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 70,
-                                     column_num = 24,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command', event_data ).json
-
-    pprint( response )
-
-    eq_( response, {
-      'detailed_info': """\
+        'location': has_entries( { 'line_num': 54, 'column_num': 16 } )
+      } )
+    )
+  } ) )
+
+
+def Subcommands_FixIt_all_test():
+  cfile = 'FixIt_Clang_cpp11.cpp'
+  mfile = 'FixIt_Clang_objc.m'
+
+  tests = [
+    [ 16, 0,  'cpp11', cfile, FixIt_Check_cpp11_Ins ],
+    [ 16, 1,  'cpp11', cfile, FixIt_Check_cpp11_Ins ],
+    [ 16, 10, 'cpp11', cfile, FixIt_Check_cpp11_Ins ],
+    [ 25, 14, 'cpp11', cfile, FixIt_Check_cpp11_InsMultiLine ],
+    [ 25, 0,  'cpp11', cfile, FixIt_Check_cpp11_InsMultiLine ],
+    [ 35, 7,  'cpp11', cfile, FixIt_Check_cpp11_Del ],
+    [ 40, 6,  'cpp11', cfile, FixIt_Check_cpp11_Repl ],
+    [ 48, 3,  'cpp11', cfile, FixIt_Check_cpp11_DelAdd ],
+
+    [ 5, 3,   'objective-c', mfile, FixIt_Check_objc ],
+    [ 7, 1,   'objective-c', mfile, FixIt_Check_objc_NoFixIt ],
+
+    # multiple errors on a single line; both with fixits
+    [ 54, 15, 'cpp11', cfile, FixIt_Check_cpp11_MultiFirst ],
+    [ 54, 16, 'cpp11', cfile, FixIt_Check_cpp11_MultiFirst ],
+    [ 54, 16, 'cpp11', cfile, FixIt_Check_cpp11_MultiFirst ],
+    [ 54, 17, 'cpp11', cfile, FixIt_Check_cpp11_MultiFirst ],
+    [ 54, 18, 'cpp11', cfile, FixIt_Check_cpp11_MultiFirst ],
+
+    # should put closest fix-it first?
+    [ 54, 51, 'cpp11', cfile, FixIt_Check_cpp11_MultiSecond ],
+    [ 54, 52, 'cpp11', cfile, FixIt_Check_cpp11_MultiSecond ],
+    [ 54, 53, 'cpp11', cfile, FixIt_Check_cpp11_MultiSecond ],
+  ]
+
+  for test in tests:
+    yield RunFixItTest, test[0], test[1], test[2], test[3], test[4]
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Variable_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 70,
+                             column_num = 24,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command', event_data ).json
+
+  pprint( response )
+
+  eq_( response, {
+    'detailed_info': """\
 char a_global_variable
 This really is a global variable.
 Type: char
@@ -719,25 +725,26 @@ def GetDoc_Variable_test( self ):
 The first line of comment is the brief.""" } )
 
 
-  def GetDoc_Method_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def Subcommands_GetDoc_Method_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 22,
-                                     column_num = 13,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 22,
+                             column_num = 13,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command', event_data ).json
+  response = app.post_json( '/run_completer_command', event_data ).json
 
-    pprint( response )
+  pprint( response )
 
-    eq_( response, {
-      'detailed_info': """\
+  eq_( response, {
+    'detailed_info': """\
 char with_brief()
 brevity is for suckers
 Type: char ()
@@ -752,25 +759,26 @@ def GetDoc_Method_test( self ):
 """ } )
 
 
-  def GetDoc_Namespace_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def Subcommands_GetDoc_Namespace_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 65,
-                                     column_num = 14,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 65,
+                             column_num = 14,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command', event_data ).json
+  response = app.post_json( '/run_completer_command', event_data ).json
 
-    pprint( response )
+  pprint( response )
 
-    eq_( response, {
-      'detailed_info': """\
+  eq_( response, {
+    'detailed_info': """\
 namespace Test {}
 This is a test namespace
 Type: 
@@ -779,80 +787,82 @@ def GetDoc_Namespace_test( self ):
 This is a test namespace""" } ) # noqa
 
 
-  def GetDoc_Undocumented_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def Subcommands_GetDoc_Undocumented_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 81,
-                                     column_num = 17,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 81,
+                             column_num = 17,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command',
-                                    event_data,
-                                    expect_errors = True )
+  response = app.post_json( '/run_completer_command',
+                            event_data,
+                            expect_errors = True )
 
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
 
-    assert_that( response.json,
-                 self._ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
+  assert_that( response.json,
+               ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
 
 
-  def GetDoc_NoCursor_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def Subcommands_GetDoc_NoCursor_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 1,
-                                     column_num = 1,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 1,
+                             column_num = 1,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command',
-                                    event_data,
-                                    expect_errors = True )
+  response = app.post_json( '/run_completer_command',
+                            event_data,
+                            expect_errors = True )
 
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
 
-    assert_that( response.json,
-                 self._ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
+  assert_that( response.json,
+               ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
 
 
-  # Following tests repeat the tests above, but without re-parsing the file
-  def GetDocQuick_Variable_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+# Following tests repeat the tests above, but without re-parsing the file
+@SharedYcmd
+def Subcommands_GetDocQuick_Variable_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    self._app.post_json( '/event_notification',
-                         self._BuildRequest( filepath = filepath,
-                                             filetype = 'cpp',
-                                             compilation_flags = [ '-x',
-                                                                   'c++' ],
-                                             contents = contents,
-                                             event_name = 'FileReadyToParse' ) )
+  app.post_json( '/event_notification',
+                 BuildRequest( filepath = filepath,
+                               filetype = 'cpp',
+                               compilation_flags = [ '-x', 'c++' ],
+                               contents = contents,
+                               event_name = 'FileReadyToParse' ) )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 70,
-                                     column_num = 24,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDocQuick' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 70,
+                             column_num = 24,
+                             contents = contents,
+                             command_arguments = [ 'GetDocQuick' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command', event_data ).json
+  response = app.post_json( '/run_completer_command', event_data ).json
 
-    pprint( response )
+  pprint( response )
 
-    eq_( response, {
-      'detailed_info': """\
+  eq_( response, {
+    'detailed_info': """\
 char a_global_variable
 This really is a global variable.
 Type: char
@@ -863,34 +873,35 @@ def GetDocQuick_Variable_test( self ):
 The first line of comment is the brief.""" } )
 
 
-  def GetDocQuick_Method_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def Subcommands_GetDocQuick_Method_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    self._app.post_json(
-      '/event_notification',
-      self._BuildRequest( filepath = filepath,
-                          filetype = 'cpp',
-                          compilation_flags = [ '-x', 'c++' ],
-                          contents = contents,
-                          event_name = 'FileReadyToParse' )
-    )
+  app.post_json(
+    '/event_notification',
+    BuildRequest( filepath = filepath,
+                  filetype = 'cpp',
+                  compilation_flags = [ '-x', 'c++' ],
+                  contents = contents,
+                  event_name = 'FileReadyToParse' )
+  )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 22,
-                                     column_num = 13,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDocQuick' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 22,
+                             column_num = 13,
+                             contents = contents,
+                             command_arguments = [ 'GetDocQuick' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command', event_data ).json
+  response = app.post_json( '/run_completer_command', event_data ).json
 
-    pprint( response )
+  pprint( response )
 
-    eq_( response, {
-      'detailed_info': """\
+  eq_( response, {
+    'detailed_info': """\
 char with_brief()
 brevity is for suckers
 Type: char ()
@@ -905,34 +916,35 @@ def GetDocQuick_Method_test( self ):
 """ } )
 
 
-  def GetDocQuick_Namespace_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def Subcommands_GetDocQuick_Namespace_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
 
-    self._app.post_json(
-      '/event_notification',
-      self._BuildRequest( filepath = filepath,
-                          filetype = 'cpp',
-                          compilation_flags = [ '-x', 'c++' ],
-                          contents = contents,
-                          event_name = 'FileReadyToParse' )
-    )
+  app.post_json(
+    '/event_notification',
+    BuildRequest( filepath = filepath,
+                  filetype = 'cpp',
+                  compilation_flags = [ '-x', 'c++' ],
+                  contents = contents,
+                  event_name = 'FileReadyToParse' )
+  )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 65,
-                                     column_num = 14,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDocQuick' ],
-                                     completer_target = 'filetype_default' )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 65,
+                             column_num = 14,
+                             contents = contents,
+                             command_arguments = [ 'GetDocQuick' ],
+                             completer_target = 'filetype_default' )
 
-    response = self._app.post_json( '/run_completer_command', event_data ).json
+  response = app.post_json( '/run_completer_command', event_data ).json
 
-    pprint( response )
+  pprint( response )
 
-    eq_( response, {
-      'detailed_info': """\
+  eq_( response, {
+    'detailed_info': """\
 namespace Test {}
 This is a test namespace
 Type: 
@@ -941,87 +953,90 @@ def GetDocQuick_Namespace_test( self ):
 This is a test namespace""" } ) # noqa
 
 
-  def GetDocQuick_Undocumented_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
-
-    self._app.post_json(
-      '/event_notification',
-      self._BuildRequest( filepath = filepath,
-                          filetype = 'cpp',
-                          compilation_flags = [ '-x', 'c++' ],
-                          contents = contents,
-                          event_name = 'FileReadyToParse' )
-    )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 81,
-                                     column_num = 17,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDocQuick' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    event_data,
-                                    expect_errors = True )
-
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-
-    assert_that( response.json,
-                 self._ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
-
-
-  def GetDocQuick_NoCursor_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
-
-    self._app.post_json(
-      '/event_notification',
-      self._BuildRequest( filepath = filepath,
-                          filetype = 'cpp',
-                          compilation_flags = [ '-x', 'c++' ],
-                          contents = contents,
-                          event_name = 'FileReadyToParse' )
-    )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 1,
-                                     column_num = 1,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDocQuick' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    event_data,
-                                    expect_errors = True )
-
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-
-    assert_that( response.json,
-                 self._ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
-
-
-  def GetDocQuick_NoReadyToParse_test( self ):
-    filepath = self._PathToTestFile( 'GetDoc_Clang.cc' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cpp',
-                                     compilation_flags = [ '-x', 'c++' ],
-                                     line_num = 11,
-                                     column_num = 18,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDocQuick' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command', event_data ).json
-
-    eq_( response, {
-      'detailed_info': """\
+@SharedYcmd
+def Subcommands_GetDocQuick_Undocumented_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
+
+  app.post_json(
+    '/event_notification',
+    BuildRequest( filepath = filepath,
+                  filetype = 'cpp',
+                  compilation_flags = [ '-x', 'c++' ],
+                  contents = contents,
+                  event_name = 'FileReadyToParse' )
+  )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 81,
+                             column_num = 17,
+                             contents = contents,
+                             command_arguments = [ 'GetDocQuick' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command',
+                            event_data,
+                            expect_errors = True )
+
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+
+  assert_that( response.json,
+               ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
+
+
+@SharedYcmd
+def Subcommands_GetDocQuick_NoCursor_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
+
+  app.post_json(
+    '/event_notification',
+    BuildRequest( filepath = filepath,
+                  filetype = 'cpp',
+                  compilation_flags = [ '-x', 'c++' ],
+                  contents = contents,
+                  event_name = 'FileReadyToParse' )
+  )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 1,
+                             column_num = 1,
+                             contents = contents,
+                             command_arguments = [ 'GetDocQuick' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command',
+                            event_data,
+                            expect_errors = True )
+
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+
+  assert_that( response.json,
+               ErrorMatcher( ValueError, NO_DOCUMENTATION_MESSAGE ) )
+
+
+@SharedYcmd
+def Subcommands_GetDocQuick_NoReadyToParse_test( app ):
+  filepath = PathToTestFile( 'GetDoc_Clang.cc' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cpp',
+                             compilation_flags = [ '-x', 'c++' ],
+                             line_num = 11,
+                             column_num = 18,
+                             contents = contents,
+                             command_arguments = [ 'GetDocQuick' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command', event_data ).json
+
+  eq_( response, {
+    'detailed_info': """\
 int get_a_global_variable(bool test)
 This is a method which is only pretend global
 Type: int (bool)
diff --git a/ycmd/tests/cs/__init__.py b/ycmd/tests/cs/__init__.py
--- a/ycmd/tests/cs/__init__.py
+++ b/ycmd/tests/cs/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2016 ycmd contributors.
+# Copyright (C) 2016 ycmd contributors
 #
 # This file is part of ycmd.
 #
@@ -15,8 +15,136 @@
 # You should have received a copy of the GNU General Public License
 # along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
 
-from .cs_handlers_test import StopAllOmniSharpServers
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
 
+from contextlib import contextmanager
+import functools
+import os
+import time
 
-def teardownPackage():
-  StopAllOmniSharpServers()
+from ycmd import handlers
+from ycmd.tests.test_utils import BuildRequest, SetUpApp
+
+shared_app = None
+shared_filepaths = []
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def StartOmniSharpServer( app, filepath ):
+  app.post_json( '/run_completer_command',
+                 BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ "StartServer" ],
+                               filepath = filepath,
+                               filetype = 'cs' ) )
+
+
+def StopOmniSharpServer( app, filepath ):
+  app.post_json( '/run_completer_command',
+                 BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'StopServer' ],
+                               filepath = filepath,
+                               filetype = 'cs' ) )
+
+
+def WaitUntilOmniSharpServerReady( app, filepath ):
+  retries = 100
+  success = False
+
+  # If running on Travis CI, keep trying forever. Travis will kill the worker
+  # after 10 mins if nothing happens.
+  while retries > 0 or OnTravis():
+    result = app.get( '/ready', { 'subserver': 'cs' } ).json
+    if result:
+      success = True
+      break
+    request = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ 'ServerIsRunning' ],
+                            filepath = filepath,
+                            filetype = 'cs' )
+    result = app.post_json( '/run_completer_command', request ).json
+    if not result:
+      raise RuntimeError( "OmniSharp failed during startup." )
+    time.sleep( 0.2 )
+    retries = retries - 1
+
+  if not success:
+    raise RuntimeError( "Timeout waiting for OmniSharpServer" )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+  shared_app.post_json(
+    '/ignore_extra_conf_file',
+    { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } )
+
+
+def tearDownPackage():
+  """Cleans up the tests using the SharedYcmd decorator in this package. It is
+  executed once after running all the tests in the package."""
+  global shared_app, shared_filepaths
+
+  for filepath in shared_filepaths:
+    StopOmniSharpServer( shared_app, filepath )
+
+
+@contextmanager
+def WrapOmniSharpServer( app, filepath ):
+  global shared_filepaths
+
+  if filepath not in shared_filepaths:
+    StartOmniSharpServer( app, filepath )
+    shared_filepaths.append( filepath )
+  WaitUntilOmniSharpServerReady( app, filepath )
+  yield
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
+
+
+def IsolatedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes a unique ycmd application as a parameter. It should be used on tests
+  that change the server state in a irreversible way (ex: a semantic subserver
+  is stopped or restarted) or expect a clean state (ex: no semantic subserver
+  started, no .ycm_extra_conf.py loaded, etc).
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    old_server_state = handlers._server_state
+
+    try:
+      app = SetUpApp()
+      app.post_json(
+        '/ignore_extra_conf_file',
+        { 'filepath': PathToTestFile( '.ycm_extra_conf.py' ) } )
+      test( app, *args, **kwargs )
+    finally:
+      handlers._server_state = old_server_state
+  return Wrapper
diff --git a/ycmd/tests/cs/cs_handlers_test.py b/ycmd/tests/cs/cs_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/cs/cs_handlers_test.py
+++ /dev/null
@@ -1,180 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-from future.utils import PY2
-
-from ..handlers_test import Handlers_test
-from ycmd.utils import OnTravis, OnWindows
-import time
-from contextlib import contextmanager
-
-# If INSTANCE_PER_TEST is set, each test case will start up and shutdown an
-# instance of Omnisharp server.  Otherwise - the default - it will reuse the
-# Omnisharp instances between individual test cases. Non caching (false) is
-# much faster, but test cases are not totally isolated from each other.
-# For test case isolation, set to true.
-# Reusing Omnisharp instances this way on Windows and Python 3 will randomly
-# raise the error "OSError: [WinError 6] The handle is invalid" in tests so
-# we set it to true in this case.
-INSTANCE_PER_TEST = True if OnWindows() and not PY2 else False
-
-
-class Cs_Handlers_test( Handlers_test ):
-
-  omnisharp_file_solution = {}
-  omnisharp_solution_port = {}
-  omnisharp_solution_file = {}
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def setUp( self ):
-    super( Cs_Handlers_test, self ).setUp()
-    self._app.post_json(
-      '/ignore_extra_conf_file',
-      { 'filepath': self._PathToTestFile( '.ycm_extra_conf.py' ) } )
-
-
-  # See __init__.py for teardownPackage
-
-
-  @contextmanager
-  def _WrapOmniSharpServer( self, filepath ):
-    self._SetupOmniSharpServer( filepath )
-    yield
-    self._TeardownOmniSharpServer( filepath )
-
-
-  def _SetupOmniSharpServer( self, filepath ):
-    solution_path = self._FindOmniSharpSolutionPath( filepath )
-    if solution_path in Cs_Handlers_test.omnisharp_solution_port:
-      port = Cs_Handlers_test.omnisharp_solution_port[ solution_path ]
-      self._SetOmnisharpPort( filepath, port  )
-      self._WaitUntilOmniSharpServerReady( filepath )
-    else:
-      self._StartOmniSharpServer( filepath )
-      self._WaitUntilOmniSharpServerReady( filepath )
-      port = self._GetOmnisharpPort( filepath )
-      Cs_Handlers_test.omnisharp_solution_port[ solution_path ] = port
-
-
-  def _TeardownOmniSharpServer( self, filepath ):
-    if INSTANCE_PER_TEST:
-      self._StopOmniSharpServer( filepath )
-      try:
-        solution = self._FindOmniSharpSolutionPath( filepath )
-        del Cs_Handlers_test.omnisharp_solution_port[ solution ]
-        del Cs_Handlers_test.omnisharp_solution_file[ solution ]
-      except KeyError:
-        pass
-
-
-  def _StartOmniSharpServer( self, filepath ):
-    self._app.post_json( '/run_completer_command',
-                    self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ "StartServer" ],
-                                        filepath = filepath,
-                                        filetype = 'cs' ) )
-
-
-  def _FindOmniSharpSolutionPath( self, filepath ):
-    if filepath in Cs_Handlers_test.omnisharp_file_solution:
-      return Cs_Handlers_test.omnisharp_file_solution[ filepath ]
-
-    solution_request = self._BuildRequest(
-        completer_target = 'filetype_default',
-        filepath = filepath,
-        command_arguments = [ "SolutionFile" ],
-        filetype = 'cs' )
-    solution_path = self._app.post_json( '/run_completer_command',
-                                         solution_request ).json
-    Cs_Handlers_test.omnisharp_file_solution[ filepath ] = solution_path
-    Cs_Handlers_test.omnisharp_solution_file[ solution_path ] = filepath
-
-    return solution_path
-
-
-  def _SetOmnisharpPort( self, filepath, port ):
-    command_arguments = [ 'SetOmnisharpPort', port ]
-    self._app.post_json( '/run_completer_command',
-                    self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = command_arguments,
-                                        filepath = filepath,
-                                        filetype = 'cs' ) )
-
-
-  def _GetOmnisharpPort( self, filepath ):
-    request = self._BuildRequest( completer_target = 'filetype_default',
-                                  command_arguments = [ "GetOmnisharpPort" ],
-                                  filepath = filepath,
-                                  filetype = 'cs' )
-    result = self._app.post_json( '/run_completer_command', request ).json
-
-    return int( result[ "message" ] )
-
-
-  def _StopOmniSharpServer( self, filepath ):
-    self._app.post_json( '/run_completer_command',
-                  self._BuildRequest( completer_target = 'filetype_default',
-                                      command_arguments = [ 'StopServer' ],
-                                      filepath = filepath,
-                                      filetype = 'cs' ) )
-
-
-  def _WaitUntilOmniSharpServerReady( self, filepath ):
-    retries = 100
-    success = False
-
-    # If running on Travis CI, keep trying forever. Travis will kill the worker
-    # after 10 mins if nothing happens.
-    while retries > 0 or OnTravis():
-      result = self._app.get( '/ready', { 'subserver': 'cs' } ).json
-      if result:
-        success = True
-        break
-      request = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ 'ServerIsRunning' ],
-                                    filepath = filepath,
-                                    filetype = 'cs' )
-      result = self._app.post_json( '/run_completer_command', request ).json
-      if not result:
-        raise RuntimeError( "OmniSharp failed during startup." )
-      time.sleep( 0.2 )
-      retries = retries - 1
-
-    if not success:
-      raise RuntimeError( "Timeout waiting for OmniSharpServer" )
-
-
-def StopAllOmniSharpServers():
-  self = Cs_Handlers_test()
-  with self.UserOption( 'auto_start_csharp_server', False ):
-    with self.UserOption( 'confirm_extra_conf', False ):
-      self.setUp()
-      while Cs_Handlers_test.omnisharp_solution_port:
-        ( solution, port ) = Cs_Handlers_test.omnisharp_solution_port.popitem()
-        filepath = Cs_Handlers_test.omnisharp_solution_file[ solution ]
-        self._SetOmnisharpPort( filepath, port )
-        self._StopOmniSharpServer( filepath )
diff --git a/ycmd/tests/cs/diagnostics_test.py b/ycmd/tests/cs/diagnostics_test.py
--- a/ycmd/tests/cs/diagnostics_test.py
+++ b/ycmd/tests/cs/diagnostics_test.py
@@ -25,114 +25,114 @@
 
 from hamcrest import ( assert_that, contains, contains_string, equal_to,
                        has_entries, has_entry )
-from .cs_handlers_test import Cs_Handlers_test
-from ycmd.utils import ReadFile
 
+from ycmd.tests.cs import PathToTestFile, SharedYcmd, WrapOmniSharpServer
+from ycmd.tests.test_utils import BuildRequest
+from ycmd.utils import ReadFile
 
-class Cs_Diagnostics_test( Cs_Handlers_test ):
 
-  def ZeroBasedLineAndColumn_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'Program.cs' )
-    with self._WrapOmniSharpServer( filepath ):
+@SharedYcmd
+def Diagnostics_Basic_test( app ):
+  filepath = PathToTestFile( 'testy', 'Program.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    event_data = BuildRequest( filepath = filepath,
+                               event_name = 'FileReadyToParse',
+                               filetype = 'cs',
+                               contents = contents )
+    app.post_json( '/event_notification', event_data )
+
+    diag_data = BuildRequest( filepath = filepath,
+                              filetype = 'cs',
+                              contents = contents,
+                              line_num = 11,
+                              column_num = 2 )
+
+    results = app.post_json( '/detailed_diagnostic', diag_data ).json
+    assert_that( results,
+                 has_entry(
+                     'message',
+                     contains_string(
+                       "Unexpected symbol `}'', expecting identifier" ) ) )
+
+
+@SharedYcmd
+def Diagnostics_ZeroBasedLineAndColumn_test( app ):
+  filepath = PathToTestFile( 'testy', 'Program.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    results = {}
+    for _ in ( 0, 1 ):  # First call always returns blank for some reason
+      event_data = BuildRequest( filepath = filepath,
+                                 event_name = 'FileReadyToParse',
+                                 filetype = 'cs',
+                                 contents = contents )
+
+      results = app.post_json( '/event_notification', event_data ).json
+
+    assert_that( results,
+                 contains(
+                     has_entries( {
+                       'kind': equal_to( 'ERROR' ),
+                       'text': contains_string(
+                           "Unexpected symbol `}'', expecting identifier" ),
+                       'location': has_entries( {
+                         'line_num': 11,
+                         'column_num': 2
+                       } ),
+                       'location_extent': has_entries( {
+                         'start': has_entries( {
+                           'line_num': 11,
+                           'column_num': 2,
+                         } ),
+                         'end': has_entries( {
+                           'line_num': 11,
+                           'column_num': 2,
+                         } ),
+                       } )
+                     } ) ) )
+
+
+@SharedYcmd
+def Diagnostics_MultipleSolution_test( app ):
+  filepaths = [ PathToTestFile( 'testy', 'Program.cs' ),
+                PathToTestFile( 'testy-multiple-solutions',
+                                'solution-named-like-folder',
+                                'testy', 'Program.cs' ) ]
+  lines = [ 11, 10 ]
+  for filepath, line in zip( filepaths, lines ):
+    with WrapOmniSharpServer( app, filepath ):
       contents = ReadFile( filepath )
 
       results = {}
-      for _ in ( 0, 1 ): # First call always returns blank for some reason
-        event_data = self._BuildRequest( filepath = filepath,
-                                         event_name = 'FileReadyToParse',
-                                         filetype = 'cs',
-                                         contents = contents )
-
-        results = self._app.post_json( '/event_notification', event_data ).json
-
-      assert_that( results,
-                  contains(
-                      has_entries( {
-                        'kind': equal_to( 'ERROR' ),
-                        'text': contains_string(
-                            "Unexpected symbol `}'', expecting identifier" ),
-                        'location': has_entries( {
-                          'line_num': 11,
-                          'column_num': 2
-                        } ),
-                        'location_extent': has_entries( {
-                          'start': has_entries( {
-                            'line_num': 11,
-                            'column_num': 2,
-                          } ),
-                          'end': has_entries( {
-                            'line_num': 11,
-                            'column_num': 2,
-                          } ),
-                        } )
-                      } ) ) )
-
-
-  def MultipleSolution_test( self ):
-    filepaths = [ self._PathToTestFile( 'testy', 'Program.cs' ),
-                  self._PathToTestFile( 'testy-multiple-solutions',
-                                        'solution-named-like-folder',
-                                        'testy',
-                                        'Program.cs' ) ]
-    lines = [ 11, 10 ]
-    for filepath, line in zip( filepaths, lines ):
-      with self._WrapOmniSharpServer( filepath ):
-        contents = ReadFile( filepath )
-
-        results = {}
-        for _ in ( 0, 1 ): # First call always returns blank for some reason
-          event_data = self._BuildRequest( filepath = filepath,
-                                           event_name = 'FileReadyToParse',
-                                           filetype = 'cs',
-                                           contents = contents )
-
-          results = self._app.post_json( '/event_notification',
-                                         event_data ).json
-
-        assert_that( results,
-                    contains(
-                        has_entries( {
-                            'kind': equal_to( 'ERROR' ),
-                            'text': contains_string( "Unexpected symbol `}'', "
-                                                      "expecting identifier" ),
-                            'location': has_entries( {
-                              'line_num': line,
-                              'column_num': 2
-                            } ),
-                            'location_extent': has_entries( {
-                              'start': has_entries( {
-                                'line_num': line,
-                                'column_num': 2,
-                              } ),
-                              'end': has_entries( {
-                                'line_num': line,
-                                'column_num': 2,
-                              } ),
-                            } )
-                        } ) ) )
-
-
-  # This test seems identical to ZeroBasedLineAndColumn one
-  def Basic_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'Program.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      event_data = self._BuildRequest( filepath = filepath,
-                                       event_name = 'FileReadyToParse',
-                                       filetype = 'cs',
-                                       contents = contents )
-      self._app.post_json( '/event_notification', event_data )
+      for _ in ( 0, 1 ):  # First call always returns blank for some reason
+        event_data = BuildRequest( filepath = filepath,
+                                   event_name = 'FileReadyToParse',
+                                   filetype = 'cs',
+                                   contents = contents )
 
-      diag_data = self._BuildRequest( filepath = filepath,
-                                      filetype = 'cs',
-                                      contents = contents,
-                                      line_num = 11,
-                                      column_num = 2 )
+        results = app.post_json( '/event_notification', event_data ).json
 
-      results = self._app.post_json( '/detailed_diagnostic', diag_data ).json
       assert_that( results,
-                  has_entry(
-                      'message',
-                      contains_string(
-                        "Unexpected symbol `}'', expecting identifier" ) ) )
+                   contains(
+                       has_entries( {
+                           'kind': equal_to( 'ERROR' ),
+                           'text': contains_string( "Unexpected symbol `}'', "
+                                                    "expecting identifier" ),
+                           'location': has_entries( {
+                             'line_num': line,
+                             'column_num': 2
+                           } ),
+                           'location_extent': has_entries( {
+                             'start': has_entries( {
+                               'line_num': line,
+                               'column_num': 2,
+                             } ),
+                             'end': has_entries( {
+                               'line_num': line,
+                               'column_num': 2,
+                             } ),
+                           } )
+                       } ) ) )
diff --git a/ycmd/tests/cs/get_completions_test.py b/ycmd/tests/cs/get_completions_test.py
--- a/ycmd/tests/cs/get_completions_test.py
+++ b/ycmd/tests/cs/get_completions_test.py
@@ -25,355 +25,373 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from webtest import AppError
-from nose.tools import eq_
 from hamcrest import ( assert_that, empty, greater_than, has_item, has_items,
                        has_entries )
-from .cs_handlers_test import Cs_Handlers_test
+from nose.tools import eq_
+from webtest import AppError
+
+from ycmd.tests.cs import PathToTestFile, SharedYcmd, WrapOmniSharpServer
+from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher
 from ycmd.utils import ReadFile
 
 
-class Cs_GetCompletions_test( Cs_Handlers_test ):
+@SharedYcmd
+def GetCompletions_Basic_test( app ):
+  filepath = PathToTestFile( 'testy', 'Program.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
 
-  def Basic_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'Program.cs' )
-    with self._WrapOmniSharpServer( filepath ):
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 10,
+                                    column_num = 12 )
+    response_data = app.post_json( '/completions', completion_data ).json
+    assert_that( response_data[ 'completions' ],
+                 has_items( CompletionEntryMatcher( 'CursorLeft' ),
+                            CompletionEntryMatcher( 'CursorSize' ) ) )
+    eq_( 12, response_data[ 'completion_start_column' ] )
+
+
+@SharedYcmd
+def GetCompletions_MultipleSolution_test( app ):
+  filepaths = [ PathToTestFile( 'testy', 'Program.cs' ),
+                PathToTestFile( 'testy-multiple-solutions',
+                                'solution-named-like-folder',
+                                'testy',
+                                'Program.cs' ) ]
+  lines = [ 10, 9 ]
+  for filepath, line in zip( filepaths, lines ):
+    with WrapOmniSharpServer( app, filepath ):
       contents = ReadFile( filepath )
 
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 10,
-                                            column_num = 12 )
-      response_data = self._app.post_json( '/completions',
-                                           completion_data ).json
+      completion_data = BuildRequest( filepath = filepath,
+                                      filetype = 'cs',
+                                      contents = contents,
+                                      line_num = line,
+                                      column_num = 12 )
+      response_data = app.post_json( '/completions',
+                                     completion_data ).json
       assert_that( response_data[ 'completions' ],
-                  has_items( self._CompletionEntryMatcher( 'CursorLeft' ),
-                              self._CompletionEntryMatcher( 'CursorSize' ) ) )
+                   has_items( CompletionEntryMatcher( 'CursorLeft' ),
+                              CompletionEntryMatcher( 'CursorSize' ) ) )
       eq_( 12, response_data[ 'completion_start_column' ] )
 
 
-  def MultipleSolution_test( self ):
-    filepaths = [ self._PathToTestFile( 'testy', 'Program.cs' ),
-                  self._PathToTestFile( 'testy-multiple-solutions',
-                                        'solution-named-like-folder',
-                                        'testy',
-                                        'Program.cs' ) ]
-    lines = [ 10, 9 ]
-    for filepath, line in zip( filepaths, lines ):
-      with self._WrapOmniSharpServer( filepath ):
-        contents = ReadFile( filepath )
-
-        completion_data = self._BuildRequest( filepath = filepath,
-                                              filetype = 'cs',
-                                              contents = contents,
-                                              line_num = line,
-                                              column_num = 12 )
-        response_data = self._app.post_json( '/completions',
-                                            completion_data ).json
-        assert_that( response_data[ 'completions' ],
-                    has_items( self._CompletionEntryMatcher( 'CursorLeft' ),
-                                self._CompletionEntryMatcher( 'CursorSize' ) ) )
-        eq_( 12, response_data[ 'completion_start_column' ] )
-
-
-  def PathWithSpace_test( self ):
-    filepath = self._PathToTestFile( u'неприличное слово', 'Program.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
+@SharedYcmd
+def GetCompletions_PathWithSpace_test( app ):
+  filepath = PathToTestFile( u'неприличное слово', 'Program.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
 
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 12 )
-      response_data = self._app.post_json( '/completions',
-                                           completion_data ).json
-      assert_that( response_data[ 'completions' ],
-                  has_items( self._CompletionEntryMatcher( 'CursorLeft' ),
-                              self._CompletionEntryMatcher( 'CursorSize' ) ) )
-      eq_( 12, response_data[ 'completion_start_column' ] )
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 12 )
+    response_data = app.post_json( '/completions', completion_data ).json
+    assert_that( response_data[ 'completions' ],
+                 has_items( CompletionEntryMatcher( 'CursorLeft' ),
+                            CompletionEntryMatcher( 'CursorSize' ) ) )
+    eq_( 12, response_data[ 'completion_start_column' ] )
+
+
+@SharedYcmd
+def GetCompletions_HasBothImportsAndNonImport_test( app ):
+  filepath = PathToTestFile( 'testy', 'ImportTest.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 12,
+                                    force_semantic = True,
+                                    query = 'Date' )
+    response_data = app.post_json( '/completions', completion_data ).json
+
+    assert_that(
+      response_data[ 'completions' ],
+      has_items( CompletionEntryMatcher( 'DateTime' ),
+                 CompletionEntryMatcher( 'DateTimeStyles' ) )
+    )
+
+
+@SharedYcmd
+def GetCompletions_ImportsOrderedAfter_test( app ):
+  filepath = PathToTestFile( 'testy', 'ImportTest.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
 
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 12,
+                                    force_semantic = True,
+                                    query = 'Date' )
+    response_data = app.post_json( '/completions', completion_data ).json
+
+    min_import_index = min(
+      loc for loc, val
+      in enumerate( response_data[ 'completions' ] )
+      if val[ 'extra_data' ][ 'required_namespace_import' ]
+    )
+
+    max_nonimport_index = max(
+      loc for loc, val
+      in enumerate( response_data[ 'completions' ] )
+      if not val[ 'extra_data' ][ 'required_namespace_import' ]
+    )
+
+    assert_that( min_import_index, greater_than( max_nonimport_index ) ),
+
+
+@SharedYcmd
+def GetCompletions_ForcedReturnsResults_test( app ):
+  filepath = PathToTestFile( 'testy', 'ContinuousTest.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
 
-  def HasBothImportsAndNonImport_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'ImportTest.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 21,
+                                    force_semantic = True,
+                                    query = 'Date' )
+    response_data = app.post_json( '/completions', completion_data ).json
 
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 12,
-                                            force_semantic = True,
-                                            query = 'Date' )
-      response_data = self._app.post_json( '/completions',
-                                           completion_data ).json
-
-      assert_that(
-        response_data[ 'completions' ],
-        has_items( self._CompletionEntryMatcher( 'DateTime' ),
-                  self._CompletionEntryMatcher( 'DateTimeStyles' ) )
-      )
-
-
-  def ImportsOrderedAfter_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'ImportTest.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
+    assert_that( response_data[ 'completions' ],
+                 has_items( CompletionEntryMatcher( 'String' ),
+                            CompletionEntryMatcher( 'StringBuilder' ) ) )
 
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 12,
-                                            force_semantic = True,
-                                            query = 'Date' )
-      response_data = self._app.post_json( '/completions',
-                                           completion_data ).json
-
-      min_import_index = min(
-        loc for loc, val
-        in enumerate( response_data[ 'completions' ] )
-        if val[ 'extra_data' ][ 'required_namespace_import' ]
-      )
-
-      max_nonimport_index = max(
-        loc for loc, val
-        in enumerate( response_data[ 'completions' ] )
-        if not val[ 'extra_data' ][ 'required_namespace_import' ]
-      )
-
-      assert_that( min_import_index, greater_than( max_nonimport_index ) ),
-
-
-  def ForcedReturnsResults_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'ContinuousTest.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
 
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 21,
-                                            force_semantic = True,
-                                            query = 'Date' )
-      response_data = self._app.post_json( '/completions',
-                                           completion_data ).json
-
-      assert_that(
-          response_data[ 'completions' ],
-          has_items( self._CompletionEntryMatcher( 'String' ),
-                     self._CompletionEntryMatcher( 'StringBuilder' ) ) )
-
-
-  def NonForcedReturnsNoResults_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'ContinuousTest.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-      event_data = self._BuildRequest( filepath = filepath,
-                                       filetype = 'cs',
-                                       contents = contents,
-                                       event_name = 'FileReadyToParse' )
-
-      self._app.post_json( '/event_notification', event_data )
-
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 21,
-                                            force_semantic = False,
-                                            query = 'Date' )
-      results = self._app.post_json( '/completions', completion_data ).json
-
-      # There are no semantic completions. However, we fall back to identifier
-      # completer in this case.
-      assert_that( results, has_entries( {
-        'completions': has_item( has_entries( {
-          'insertion_text' : 'String',
-          'extra_menu_info': '[ID]',
-        } ) ),
-        'errors': empty(),
-      } ) )
-
-
-  def ForcedDividesCache_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'ContinuousTest.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-      event_data = self._BuildRequest( filepath = filepath,
-                                       filetype = 'cs',
-                                       contents = contents,
-                                       event_name = 'FileReadyToParse' )
-
-      self._app.post_json( '/event_notification', event_data )
-
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 21,
-                                            force_semantic = True,
-                                            query = 'Date' )
-      results = self._app.post_json( '/completions', completion_data ).json
-
-      assert_that( results[ 'completions' ], not( empty() ) )
-      assert_that( results[ 'errors' ], empty() )
-
-      completion_data = self._BuildRequest( filepath = filepath,
-                                            filetype = 'cs',
-                                            contents = contents,
-                                            line_num = 9,
-                                            column_num = 21,
-                                            force_semantic = False,
-                                            query = 'Date' )
-      results = self._app.post_json( '/completions', completion_data ).json
-
-      # There are no semantic completions. However, we fall back to identifier
-      # completer in this case.
-      assert_that( results, has_entries( {
-        'completions': has_item( has_entries( {
-          'insertion_text' : 'String',
-          'extra_menu_info': '[ID]',
-        } ) ),
-        'errors': empty(),
-      } ) )
-
-
-  def ReloadSolution_Basic_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'Program.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      result = self._app.post_json(
+@SharedYcmd
+def GetCompletions_NonForcedReturnsNoResults_test( app ):
+  filepath = PathToTestFile( 'testy', 'ContinuousTest.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+    event_data = BuildRequest( filepath = filepath,
+                               filetype = 'cs',
+                               contents = contents,
+                               event_name = 'FileReadyToParse' )
+
+    app.post_json( '/event_notification', event_data )
+
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 21,
+                                    force_semantic = False,
+                                    query = 'Date' )
+    results = app.post_json( '/completions', completion_data ).json
+
+    # There are no semantic completions. However, we fall back to identifier
+    # completer in this case.
+    assert_that( results, has_entries( {
+      'completions': has_item( has_entries( {
+        'insertion_text' : 'String',
+        'extra_menu_info': '[ID]',
+      } ) ),
+      'errors': empty(),
+    } ) )
+
+
+@SharedYcmd
+def GetCompletions_ForcedDividesCache_test( app ):
+  filepath = PathToTestFile( 'testy', 'ContinuousTest.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+    event_data = BuildRequest( filepath = filepath,
+                               filetype = 'cs',
+                               contents = contents,
+                               event_name = 'FileReadyToParse' )
+
+    app.post_json( '/event_notification', event_data )
+
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 21,
+                                    force_semantic = True,
+                                    query = 'Date' )
+    results = app.post_json( '/completions', completion_data ).json
+
+    assert_that( results[ 'completions' ], not( empty() ) )
+    assert_that( results[ 'errors' ], empty() )
+
+    completion_data = BuildRequest( filepath = filepath,
+                                    filetype = 'cs',
+                                    contents = contents,
+                                    line_num = 9,
+                                    column_num = 21,
+                                    force_semantic = False,
+                                    query = 'Date' )
+    results = app.post_json( '/completions', completion_data ).json
+
+    # There are no semantic completions. However, we fall back to identifier
+    # completer in this case.
+    assert_that( results, has_entries( {
+      'completions': has_item( has_entries( {
+        'insertion_text' : 'String',
+        'extra_menu_info': '[ID]',
+      } ) ),
+      'errors': empty(),
+    } ) )
+
+
+@SharedYcmd
+def GetCompletions_ReloadSolution_Basic_test( app ):
+  filepath = PathToTestFile( 'testy', 'Program.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    result = app.post_json(
+      '/run_completer_command',
+      BuildRequest( completer_target = 'filetype_default',
+                    command_arguments = [ 'ReloadSolution' ],
+                    filepath = filepath,
+                    filetype = 'cs' ) ).json
+
+    eq_( result, True )
+
+
+@SharedYcmd
+def GetCompletions_ReloadSolution_MultipleSolution_test( app ):
+  filepaths = [ PathToTestFile( 'testy', 'Program.cs' ),
+                PathToTestFile( 'testy-multiple-solutions',
+                                'solution-named-like-folder',
+                                'testy',
+                                'Program.cs' ) ]
+  for filepath in filepaths:
+    with WrapOmniSharpServer( app, filepath ):
+      result = app.post_json(
         '/run_completer_command',
-        self._BuildRequest( completer_target = 'filetype_default',
-                            command_arguments = [ 'ReloadSolution' ],
-                            filepath = filepath,
-                            filetype = 'cs' ) ).json
+        BuildRequest( completer_target = 'filetype_default',
+                      command_arguments = [ 'ReloadSolution' ],
+                      filepath = filepath,
+                      filetype = 'cs' ) ).json
 
       eq_( result, True )
 
 
-  def ReloadSolution_MultipleSolution_test( self ):
-    filepaths = [ self._PathToTestFile( 'testy', 'Program.cs' ),
-                  self._PathToTestFile( 'testy-multiple-solutions',
-                                        'solution-named-like-folder',
-                                        'testy',
-                                        'Program.cs' ) ]
-    for filepath in filepaths:
-      with self._WrapOmniSharpServer( filepath ):
-        result = self._app.post_json(
-          '/run_completer_command',
-          self._BuildRequest( completer_target = 'filetype_default',
-                              command_arguments = [ 'ReloadSolution' ],
-                              filepath = filepath,
-                              filetype = 'cs' ) ).json
-
-        eq_( result, True )
-
-
-  def _SolutionSelectCheck( self, sourcefile, reference_solution,
-                            extra_conf_store = None ):
-    # reusable test: verify that the correct solution (reference_solution) is
-    #   detected for a given source file (and optionally a given extra_conf)
-    if extra_conf_store:
-      self._app.post_json( '/load_extra_conf_file',
-                           { 'filepath': extra_conf_store } )
-
-    result = self._app.post_json(
-      '/run_completer_command',
-      self._BuildRequest( completer_target = 'filetype_default',
-                          command_arguments = [ 'SolutionFile' ],
-                          filepath = sourcefile,
-                          filetype = 'cs' ) ).json
-
-    # Now that cleanup is done, verify solution file
-    eq_( reference_solution, result)
-
-
-  def UsesSubfolderHint_test( self ):
-    self._SolutionSelectCheck(
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-named-like-folder',
-                            'testy', 'Program.cs' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-named-like-folder',
-                            'testy.sln' ) )
-
-
-  def UsesSuperfolderHint_test( self ):
-    self._SolutionSelectCheck(
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-named-like-folder',
-                            'not-testy', 'Program.cs' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-named-like-folder',
-                            'solution-named-like-folder.sln' ) )
-
-
-  def ExtraConfStoreAbsolute_test( self ):
-    self._SolutionSelectCheck(
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-abs',
-                            'testy', 'Program.cs' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'testy2.sln' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-abs',
-                            '.ycm_extra_conf.py' ) )
-
-
-  def ExtraConfStoreRelative_test( self ):
-    self._SolutionSelectCheck(
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-rel',
-                            'testy', 'Program.cs' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-rel',
-                            'testy2.sln' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-rel',
-                            '.ycm_extra_conf.py' ) )
-
-
-  def ExtraConfStoreNonexisting_test( self ):
-    self._SolutionSelectCheck(
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-bad',
-                            'testy', 'Program.cs' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-bad',
-                            'testy2.sln' ),
-      self._PathToTestFile( 'testy-multiple-solutions',
-                            'solution-not-named-like-folder', 'extra-conf-bad',
-                            'testy', '.ycm_extra_conf.py' ) )
-
-
-  def DoesntStartWithAmbiguousMultipleSolutions_test( self ):
-    filepath = self._PathToTestFile( 'testy-multiple-solutions',
-                                     'solution-not-named-like-folder',
-                                     'testy', 'Program.cs' )
-    contents = ReadFile( filepath )
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'cs',
-                                     contents = contents,
-                                     event_name = 'FileReadyToParse' )
-
-    exception_caught = False
-    try:
-      self._app.post_json( '/event_notification', event_data )
-    except AppError as e:
-      if 'Autodetection of solution file failed' in str( e ):
-        exception_caught = True
-
-    # The test passes if we caught an exception when trying to start it,
-    # so raise one if it managed to start
-    if not exception_caught:
-      self._WaitUntilOmniSharpServerReady( filepath )
-      self._StopOmniSharpServer( filepath )
-      raise Exception( 'The Omnisharp server started, despite us not being '
-                       'able to find a suitable solution file to feed it. Did '
-                       'you fiddle with the solution finding code in '
-                       'cs_completer.py? Hopefully you\'ve enhanced it: you '
-                       'need to update this test then :)' )
+def SolutionSelectCheck( app, sourcefile, reference_solution,
+                         extra_conf_store = None ):
+  # reusable test: verify that the correct solution (reference_solution) is
+  #   detected for a given source file (and optionally a given extra_conf)
+  if extra_conf_store:
+    app.post_json( '/load_extra_conf_file',
+                   { 'filepath': extra_conf_store } )
+
+  result = app.post_json( '/run_completer_command',
+                          BuildRequest( completer_target = 'filetype_default',
+                                        command_arguments = [ 'SolutionFile' ],
+                                        filepath = sourcefile,
+                                        filetype = 'cs' ) ).json
+
+  # Now that cleanup is done, verify solution file
+  eq_( reference_solution, result)
+
+
+@SharedYcmd
+def GetCompletions_UsesSubfolderHint_test( app ):
+  SolutionSelectCheck( app,
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-named-like-folder',
+                                       'testy', 'Program.cs' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-named-like-folder',
+                                       'testy.sln' ) )
+
+
+@SharedYcmd
+def GetCompletions_UsesSuperfolderHint_test( app ):
+  SolutionSelectCheck( app,
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-named-like-folder',
+                                       'not-testy', 'Program.cs' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-named-like-folder',
+                                       'solution-named-like-folder.sln' ) )
+
+
+@SharedYcmd
+def GetCompletions_ExtraConfStoreAbsolute_test( app ):
+  SolutionSelectCheck( app,
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-abs',
+                                       'testy', 'Program.cs' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'testy2.sln' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-abs',
+                                       '.ycm_extra_conf.py' ) )
+
+
+@SharedYcmd
+def GetCompletions_ExtraConfStoreRelative_test( app ):
+  SolutionSelectCheck( app,
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-rel',
+                                       'testy', 'Program.cs' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-rel',
+                                       'testy2.sln' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-rel',
+                                       '.ycm_extra_conf.py' ) )
+
+
+@SharedYcmd
+def GetCompletions_ExtraConfStoreNonexisting_test( app ):
+  SolutionSelectCheck( app,
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-bad',
+                                       'testy', 'Program.cs' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-bad',
+                                       'testy2.sln' ),
+                       PathToTestFile( 'testy-multiple-solutions',
+                                       'solution-not-named-like-folder',
+                                       'extra-conf-bad',
+                                       'testy', '.ycm_extra_conf.py' ) )
+
+
+@SharedYcmd
+def GetCompletions_DoesntStartWithAmbiguousMultipleSolutions_test( app ):
+  filepath = PathToTestFile( 'testy-multiple-solutions',
+                             'solution-not-named-like-folder',
+                             'testy', 'Program.cs' )
+  contents = ReadFile( filepath )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'cs',
+                             contents = contents,
+                             event_name = 'FileReadyToParse' )
+
+  exception_caught = False
+  try:
+    app.post_json( '/event_notification', event_data )
+  except AppError as e:
+    if 'Autodetection of solution file failed' in str( e ):
+      exception_caught = True
+
+  # The test passes if we caught an exception when trying to start it,
+  # so raise one if it managed to start
+  if not exception_caught:
+    WaitUntilOmniSharpServerReady( app, filepath )
+    StopOmniSharpServer( app, filepath )
+    raise Exception( 'The Omnisharp server started, despite us not being '
+                     'able to find a suitable solution file to feed it. Did '
+                     'you fiddle with the solution finding code in '
+                     'cs_completer.py? Hopefully you\'ve enhanced it: you '
+                     'need to update this test then :)' )
diff --git a/ycmd/tests/cs/subcommands_test.py b/ycmd/tests/cs/subcommands_test.py
--- a/ycmd/tests/cs/subcommands_test.py
+++ b/ycmd/tests/cs/subcommands_test.py
@@ -23,542 +23,562 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from webtest import TestApp, AppError
 from nose.tools import eq_, ok_
-from ... import handlers
-from .cs_handlers_test import Cs_Handlers_test
-from ycmd.utils import ReadFile
+from webtest import AppError
 import re
 import os.path
 
+from ycmd.tests.cs import ( IsolatedYcmd, PathToTestFile, SharedYcmd,
+                            StopOmniSharpServer, WaitUntilOmniSharpServerReady,
+                            WrapOmniSharpServer )
+from ycmd.tests.test_utils import BuildRequest, UserOption
+from ycmd.utils import ReadFile
+
 
-class Cs_Subcommands_test( Cs_Handlers_test ):
-
-  def GoTo_Basic_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                      command_arguments = [ 'GoTo' ],
-                                      line_num = 9,
-                                      column_num = 15,
-                                      contents = contents,
-                                      filetype = 'cs',
-                                      filepath = filepath )
-
-      eq_( {
-        'filepath': self._PathToTestFile( 'testy', 'Program.cs' ),
-        'line_num': 7,
-        'column_num': 3
-      }, self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoToImplementation_Basic_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = [ 'GoToImplementation' ],
-        line_num = 13,
-        column_num = 13,
-        contents = contents,
-        filetype = 'cs',
-        filepath = filepath
-      )
-
-      eq_( {
-        'filepath': self._PathToTestFile( 'testy', 'GotoTestCase.cs' ),
-        'line_num': 30,
-        'column_num': 3
-      }, self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoToImplementation_NoImplementation_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = [ 'GoToImplementation' ],
-        line_num = 17,
-        column_num = 13,
-        contents = contents,
-        filetype = 'cs',
-        filepath = filepath
-      )
-
-      try:
-        self._app.post_json( '/run_completer_command', goto_data ).json
-        raise Exception("Expected a 'No implementations found' error")
-      except AppError as e:
-        if 'No implementations found' in str(e):
-          pass
-        else:
-          raise
-
-
-  def CsCompleter_InvalidLocation_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = [ 'GoToImplementation' ],
-        line_num = 2,
-        column_num = 1,
-        contents = contents,
-        filetype = 'cs',
-        filepath = filepath
-      )
-
-      try:
-        self._app.post_json( '/run_completer_command', goto_data ).json
-        raise Exception( 'Expected a "Can\\\'t jump to implementation" error' )
-      except AppError as e:
-        if 'Can\\\'t jump to implementation' in str(e):
-          pass
-        else:
-          raise
-
-
-  def GoToImplementationElseDeclaration_NoImplementation_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = [ 'GoToImplementationElseDeclaration' ],
-        line_num = 17,
-        column_num = 13,
-        contents = contents,
-        filetype = 'cs',
-        filepath = filepath
-      )
-
-      eq_( {
-        'filepath': self._PathToTestFile( 'testy', 'GotoTestCase.cs' ),
-        'line_num': 35,
-        'column_num': 3
-      }, self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoToImplementationElseDeclaration_SingleImplementation_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = [ 'GoToImplementationElseDeclaration' ],
-        line_num = 13,
-        column_num = 13,
-        contents = contents,
-        filetype = 'cs',
-        filepath = filepath
-      )
-
-      eq_( {
-        'filepath': self._PathToTestFile( 'testy', 'GotoTestCase.cs' ),
-        'line_num': 30,
-        'column_num': 3
-      }, self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoToImplementationElseDeclaration_MultipleImplementations_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = [ 'GoToImplementationElseDeclaration' ],
-        line_num = 21,
-        column_num = 13,
-        contents = contents,
-        filetype = 'cs',
-        filepath = filepath
-      )
-
-      eq_( [ {
-        'filepath': self._PathToTestFile( 'testy', 'GotoTestCase.cs' ),
-        'line_num': 43,
-        'column_num': 3
-      }, {
-        'filepath': self._PathToTestFile( 'testy', 'GotoTestCase.cs' ),
-        'line_num': 48,
-        'column_num': 3
-      } ], self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GetType_EmptyMessage_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetType' ],
-                                        line_num = 1,
-                                        column_num = 1,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      eq_( {
-        u'message': u""
-      }, self._app.post_json( '/run_completer_command', gettype_data ).json )
-
-
-  def GetType_VariableDeclaration_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetType' ],
-                                        line_num = 4,
-                                        column_num = 5,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      eq_( {
-        u'message': u"string"
-      }, self._app.post_json( '/run_completer_command', gettype_data ).json )
-
-
-  def GetType_VariableUsage_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetType' ],
-                                        line_num = 5,
-                                        column_num = 5,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      eq_( {
-        u'message': u"string str"
-      }, self._app.post_json( '/run_completer_command', gettype_data ).json )
-
-
-  def GetType_Constant_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetType' ],
-                                        line_num = 4,
-                                        column_num = 14,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      eq_( {
-        u'message': u"System.String"
-      }, self._app.post_json( '/run_completer_command', gettype_data ).json )
-
-
-  def GetType_DocsIgnored_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetType' ],
-                                        line_num = 9,
-                                        column_num = 34,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      eq_( {
-        u'message': u"int GetTypeTestCase.an_int_with_docs;",
-      }, self._app.post_json( '/run_completer_command', gettype_data ).json )
-
-
-  def GetDoc_Variable_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetDocTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      getdoc_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetDoc' ],
-                                        line_num = 13,
-                                        column_num = 28,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      eq_( {
-        'detailed_info': 'int GetDocTestCase.an_int;\n'
-                        'an integer, or something',
-      }, self._app.post_json( '/run_completer_command', getdoc_data ).json )
-
-
-  def GetDoc_Function_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GetDocTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      getdoc_data = self._BuildRequest( completer_target = 'filetype_default',
-                                        command_arguments = [ 'GetDoc' ],
-                                        line_num = 33,
-                                        column_num = 27,
-                                        contents = contents,
-                                        filetype = 'cs',
-                                        filepath = filepath )
-
-      # It seems that Omnisharp server eats newlines
-      eq_( {
-        'detailed_info': 'int GetDocTestCase.DoATest();\n'
-                        ' Very important method. With multiple lines of '
-                        'commentary And Format- -ting',
-      }, self._app.post_json( '/run_completer_command', getdoc_data ).json )
-
-
-  def _RunFixIt( self, line, column, expected_result ):
-    filepath = self._PathToTestFile( 'testy', 'FixItTestCase.cs' )
-    with self._WrapOmniSharpServer( filepath ):
-      contents = ReadFile( filepath )
-
-      fixit_data = self._BuildRequest( completer_target = 'filetype_default',
-                                      command_arguments = [ 'FixIt' ],
-                                      line_num = line,
-                                      column_num = column,
-                                      contents = contents,
-                                      filetype = 'cs',
-                                      filepath = filepath )
-
-      eq_( expected_result,
-          self._app.post_json( '/run_completer_command', fixit_data ).json )
-
-
-  def FixIt_RemoveSingleLine_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'FixItTestCase.cs' )
-    self._RunFixIt( 11, 1, {
-      u'fixits': [
-        {
-          u'location': {
-            u'line_num': 11,
-            u'column_num': 1,
-            u'filepath': filepath
-          },
-          u'chunks': [
-            {
-              u'replacement_text': '',
-              u'range': {
-                u'start': {
-                  u'line_num': 10,
-                  u'column_num': 20,
-                  u'filepath': filepath
-                },
-                u'end': {
-                  u'line_num': 11,
-                  u'column_num': 30,
-                  u'filepath': filepath
-                },
-              }
+@SharedYcmd
+def Subcommands_GoTo_Basic_test( app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest( completer_target = 'filetype_default',
+                              command_arguments = [ 'GoTo' ],
+                              line_num = 9,
+                              column_num = 15,
+                              contents = contents,
+                              filetype = 'cs',
+                              filepath = filepath )
+
+    eq_( {
+      'filepath': PathToTestFile( 'testy', 'Program.cs' ),
+      'line_num': 7,
+      'column_num': 3
+    }, app.post_json( '/run_completer_command', goto_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GoToImplementation_Basic_test( app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = [ 'GoToImplementation' ],
+      line_num = 13,
+      column_num = 13,
+      contents = contents,
+      filetype = 'cs',
+      filepath = filepath
+    )
+
+    eq_( {
+      'filepath': PathToTestFile( 'testy', 'GotoTestCase.cs' ),
+      'line_num': 30,
+      'column_num': 3
+    }, app.post_json( '/run_completer_command', goto_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GoToImplementation_NoImplementation_test( app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = [ 'GoToImplementation' ],
+      line_num = 17,
+      column_num = 13,
+      contents = contents,
+      filetype = 'cs',
+      filepath = filepath
+    )
+
+    try:
+      app.post_json( '/run_completer_command', goto_data ).json
+      raise Exception("Expected a 'No implementations found' error")
+    except AppError as e:
+      if 'No implementations found' in str(e):
+        pass
+      else:
+        raise
+
+
+@SharedYcmd
+def Subcommands_CsCompleter_InvalidLocation_test( app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = [ 'GoToImplementation' ],
+      line_num = 2,
+      column_num = 1,
+      contents = contents,
+      filetype = 'cs',
+      filepath = filepath
+    )
+
+    try:
+      app.post_json( '/run_completer_command', goto_data ).json
+      raise Exception( 'Expected a "Can\\\'t jump to implementation" error' )
+    except AppError as e:
+      if 'Can\\\'t jump to implementation' in str(e):
+        pass
+      else:
+        raise
+
+
+@SharedYcmd
+def Subcommands_GoToImplementationElseDeclaration_NoImplementation_test( app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = [ 'GoToImplementationElseDeclaration' ],
+      line_num = 17,
+      column_num = 13,
+      contents = contents,
+      filetype = 'cs',
+      filepath = filepath
+    )
+
+    eq_( {
+      'filepath': PathToTestFile( 'testy', 'GotoTestCase.cs' ),
+      'line_num': 35,
+      'column_num': 3
+    }, app.post_json( '/run_completer_command', goto_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GoToImplementationElseDeclaration_SingleImplementation_test(
+  app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = [ 'GoToImplementationElseDeclaration' ],
+      line_num = 13,
+      column_num = 13,
+      contents = contents,
+      filetype = 'cs',
+      filepath = filepath
+    )
+
+    eq_( {
+      'filepath': PathToTestFile( 'testy', 'GotoTestCase.cs' ),
+      'line_num': 30,
+      'column_num': 3
+    }, app.post_json( '/run_completer_command', goto_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GoToImplementationElseDeclaration_MultipleImplementations_test(
+  app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = [ 'GoToImplementationElseDeclaration' ],
+      line_num = 21,
+      column_num = 13,
+      contents = contents,
+      filetype = 'cs',
+      filepath = filepath
+    )
+
+    eq_( [ {
+      'filepath': PathToTestFile( 'testy', 'GotoTestCase.cs' ),
+      'line_num': 43,
+      'column_num': 3
+    }, {
+      'filepath': PathToTestFile( 'testy', 'GotoTestCase.cs' ),
+      'line_num': 48,
+      'column_num': 3
+    } ], app.post_json( '/run_completer_command', goto_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetType_EmptyMessage_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    gettype_data = BuildRequest( completer_target = 'filetype_default',
+                                 command_arguments = [ 'GetType' ],
+                                 line_num = 1,
+                                 column_num = 1,
+                                 contents = contents,
+                                 filetype = 'cs',
+                                 filepath = filepath )
+
+    eq_( {
+      u'message': u""
+    }, app.post_json( '/run_completer_command', gettype_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetType_VariableDeclaration_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    gettype_data = BuildRequest( completer_target = 'filetype_default',
+                                 command_arguments = [ 'GetType' ],
+                                 line_num = 4,
+                                 column_num = 5,
+                                 contents = contents,
+                                 filetype = 'cs',
+                                 filepath = filepath )
+
+    eq_( {
+      u'message': u"string"
+    }, app.post_json( '/run_completer_command', gettype_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetType_VariableUsage_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    gettype_data = BuildRequest( completer_target = 'filetype_default',
+                                 command_arguments = [ 'GetType' ],
+                                 line_num = 5,
+                                 column_num = 5,
+                                 contents = contents,
+                                 filetype = 'cs',
+                                 filepath = filepath )
+
+    eq_( {
+      u'message': u"string str"
+    }, app.post_json( '/run_completer_command', gettype_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetType_Constant_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    gettype_data = BuildRequest( completer_target = 'filetype_default',
+                                 command_arguments = [ 'GetType' ],
+                                 line_num = 4,
+                                 column_num = 14,
+                                 contents = contents,
+                                 filetype = 'cs',
+                                 filepath = filepath )
+
+    eq_( {
+      u'message': u"System.String"
+    }, app.post_json( '/run_completer_command', gettype_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetType_DocsIgnored_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetTypeTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    gettype_data = BuildRequest( completer_target = 'filetype_default',
+                                 command_arguments = [ 'GetType' ],
+                                 line_num = 9,
+                                 column_num = 34,
+                                 contents = contents,
+                                 filetype = 'cs',
+                                 filepath = filepath )
+
+    eq_( {
+      u'message': u"int GetTypeTestCase.an_int_with_docs;",
+    }, app.post_json( '/run_completer_command', gettype_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Variable_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetDocTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    getdoc_data = BuildRequest( completer_target = 'filetype_default',
+                                command_arguments = [ 'GetDoc' ],
+                                line_num = 13,
+                                column_num = 28,
+                                contents = contents,
+                                filetype = 'cs',
+                                filepath = filepath )
+
+    eq_( {
+      'detailed_info': 'int GetDocTestCase.an_int;\n'
+                       'an integer, or something',
+    }, app.post_json( '/run_completer_command', getdoc_data ).json )
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Function_test( app ):
+  filepath = PathToTestFile( 'testy', 'GetDocTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    getdoc_data = BuildRequest( completer_target = 'filetype_default',
+                                command_arguments = [ 'GetDoc' ],
+                                line_num = 33,
+                                column_num = 27,
+                                contents = contents,
+                                filetype = 'cs',
+                                filepath = filepath )
+
+    # It seems that Omnisharp server eats newlines
+    eq_( {
+      'detailed_info': 'int GetDocTestCase.DoATest();\n'
+                       ' Very important method. With multiple lines of '
+                       'commentary And Format- -ting',
+    }, app.post_json( '/run_completer_command', getdoc_data ).json )
+
+
+def RunFixItTest( app, line, column, expected_result ):
+  filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' )
+  with WrapOmniSharpServer( app, filepath ):
+    contents = ReadFile( filepath )
+
+    fixit_data = BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'FixIt' ],
+                               line_num = line,
+                               column_num = column,
+                               contents = contents,
+                               filetype = 'cs',
+                               filepath = filepath )
+
+    eq_( expected_result,
+         app.post_json( '/run_completer_command', fixit_data ).json )
+
+
+@SharedYcmd
+def Subcommands_FixIt_RemoveSingleLine_test( app ):
+  filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' )
+  RunFixItTest( app, 11, 1, {
+    u'fixits': [
+      {
+        u'location': {
+          u'line_num': 11,
+          u'column_num': 1,
+          u'filepath': filepath
+        },
+        u'chunks': [
+          {
+            u'replacement_text': '',
+            u'range': {
+              u'start': {
+                u'line_num': 10,
+                u'column_num': 20,
+                u'filepath': filepath
+              },
+              u'end': {
+                u'line_num': 11,
+                u'column_num': 30,
+                u'filepath': filepath
+              },
             }
-          ]
-        }
-      ]
-    } )
-
-
-  def FixIt_MultipleLines_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'FixItTestCase.cs' )
-    self._RunFixIt( 19, 1, {
-      u'fixits': [
-        {
-          u'location': {
-            u'line_num': 19,
-            u'column_num': 1,
-            u'filepath': filepath
-          },
-          u'chunks': [
-            {
-              u'replacement_text': "return On",
-              u'range': {
-                u'start': {
-                  u'line_num': 20,
-                  u'column_num': 13,
-                  u'filepath': filepath
-                },
-                u'end': {
-                  u'line_num': 21,
-                  u'column_num': 35,
-                  u'filepath': filepath
-                },
-              }
+          }
+        ]
+      }
+    ]
+  } )
+
+
+@SharedYcmd
+def Subcommands_FixIt_MultipleLines_test( app ):
+  filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' )
+  RunFixItTest( app, 19, 1, {
+    u'fixits': [
+      {
+        u'location': {
+          u'line_num': 19,
+          u'column_num': 1,
+          u'filepath': filepath
+        },
+        u'chunks': [
+          {
+            u'replacement_text': "return On",
+            u'range': {
+              u'start': {
+                u'line_num': 20,
+                u'column_num': 13,
+                u'filepath': filepath
+              },
+              u'end': {
+                u'line_num': 21,
+                u'column_num': 35,
+                u'filepath': filepath
+              },
             }
-          ]
-        }
-      ]
-    } )
-
-
-  def FixIt_SpanFileEdge_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'FixItTestCase.cs' )
-    self._RunFixIt( 1, 1, {
-      u'fixits': [
-        {
-          u'location': {
-            u'line_num': 1,
-            u'column_num': 1,
-            u'filepath': filepath
-          },
-          u'chunks': [
-            {
-              u'replacement_text': 'System',
-              u'range': {
-                u'start': {
-                  u'line_num': 1,
-                  u'column_num': 7,
-                  u'filepath': filepath
-                },
-                u'end': {
-                  u'line_num': 3,
-                  u'column_num': 18,
-                  u'filepath': filepath
-                },
-              }
+          }
+        ]
+      }
+    ]
+  } )
+
+
+@SharedYcmd
+def Subcommands_FixIt_SpanFileEdge_test( app ):
+  filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' )
+  RunFixItTest( app, 1, 1, {
+    u'fixits': [
+      {
+        u'location': {
+          u'line_num': 1,
+          u'column_num': 1,
+          u'filepath': filepath
+        },
+        u'chunks': [
+          {
+            u'replacement_text': 'System',
+            u'range': {
+              u'start': {
+                u'line_num': 1,
+                u'column_num': 7,
+                u'filepath': filepath
+              },
+              u'end': {
+                u'line_num': 3,
+                u'column_num': 18,
+                u'filepath': filepath
+              },
             }
-          ]
-        }
-      ]
-    } )
-
-
-  def FixIt_AddTextInLine_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'FixItTestCase.cs' )
-    self._RunFixIt( 9, 1, {
-      u'fixits': [
-        {
-          u'location': {
-            u'line_num': 9,
-            u'column_num': 1,
-            u'filepath': filepath
-          },
-          u'chunks': [
-            {
-              u'replacement_text': ', StringComparison.Ordinal',
-              u'range': {
-                u'start': {
-                  u'line_num': 9,
-                  u'column_num': 29,
-                  u'filepath': filepath
-                },
-                u'end': {
-                  u'line_num': 9,
-                  u'column_num': 29,
-                  u'filepath': filepath
-                },
-              }
+          }
+        ]
+      }
+    ]
+  } )
+
+
+@SharedYcmd
+def Subcommands_FixIt_AddTextInLine_test( app ):
+  filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' )
+  RunFixItTest( app, 9, 1, {
+    u'fixits': [
+      {
+        u'location': {
+          u'line_num': 9,
+          u'column_num': 1,
+          u'filepath': filepath
+        },
+        u'chunks': [
+          {
+            u'replacement_text': ', StringComparison.Ordinal',
+            u'range': {
+              u'start': {
+                u'line_num': 9,
+                u'column_num': 29,
+                u'filepath': filepath
+              },
+              u'end': {
+                u'line_num': 9,
+                u'column_num': 29,
+                u'filepath': filepath
+              },
             }
-          ]
-        }
-      ]
-    } )
-
-
-  def FixIt_ReplaceTextInLine_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'FixItTestCase.cs' )
-    self._RunFixIt( 10, 1, {
-      u'fixits': [
-        {
-          u'location': {
-            u'line_num': 10,
-            u'column_num': 1,
-            u'filepath': filepath
-          },
-          u'chunks': [
-            {
-              u'replacement_text': 'const int',
-              u'range': {
-                u'start': {
-                  u'line_num': 10,
-                  u'column_num': 13,
-                  u'filepath': filepath
-                },
-                u'end': {
-                  u'line_num': 10,
-                  u'column_num': 16,
-                  u'filepath': filepath
-                },
-              }
+          }
+        ]
+      }
+    ]
+  } )
+
+
+@SharedYcmd
+def Subcommands_FixIt_ReplaceTextInLine_test( app ):
+  filepath = PathToTestFile( 'testy', 'FixItTestCase.cs' )
+  RunFixItTest( app, 10, 1, {
+    u'fixits': [
+      {
+        u'location': {
+          u'line_num': 10,
+          u'column_num': 1,
+          u'filepath': filepath
+        },
+        u'chunks': [
+          {
+            u'replacement_text': 'const int',
+            u'range': {
+              u'start': {
+                u'line_num': 10,
+                u'column_num': 13,
+                u'filepath': filepath
+              },
+              u'end': {
+                u'line_num': 10,
+                u'column_num': 16,
+                u'filepath': filepath
+              },
             }
-          ]
-        }
-      ]
-    } )
-
-
-  def StopServer_NoErrorIfNotStarted_test( self ):
-    filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-    self._StopOmniSharpServer( filepath )
-    # Success = no raise
-
-
-  def StopServer_KeepLogFiles_test( self ):
-    yield self._StopServer_KeepLogFiles, True
-    yield self._StopServer_KeepLogFiles, False
-
-
-  def _StopServer_KeepLogFiles( self, keeping_log_files ):
-    with self.UserOption( 'server_keep_logfiles', keeping_log_files ):
-      self._app = TestApp( handlers.app )
-      self._app.post_json(
-        '/ignore_extra_conf_file',
-        { 'filepath': self._PathToTestFile( '.ycm_extra_conf.py' ) } )
-      filepath = self._PathToTestFile( 'testy', 'GotoTestCase.cs' )
-      contents = ReadFile( filepath )
-      event_data = self._BuildRequest( filepath = filepath,
-                                      filetype = 'cs',
-                                      contents = contents,
-                                      event_name = 'FileReadyToParse' )
-
-      self._app.post_json( '/event_notification', event_data )
-      self._WaitUntilOmniSharpServerReady( filepath )
-
-      event_data = self._BuildRequest( filetype = 'cs', filepath = filepath )
-
-      debuginfo = self._app.post_json( '/debug_info', event_data ).json
-
-      log_files_match = re.search( "^OmniSharp logfiles:\n(.*)\n(.*)",
-                                  debuginfo,
-                                  re.MULTILINE )
-      stdout_logfiles_location = log_files_match.group( 1 )
-      stderr_logfiles_location = log_files_match.group( 2 )
-
-      try:
-        ok_( os.path.exists(stdout_logfiles_location ),
-            "Logfile should exist at {0}".format( stdout_logfiles_location ) )
-        ok_( os.path.exists( stderr_logfiles_location ),
-            "Logfile should exist at {0}".format( stderr_logfiles_location ) )
-      finally:
-        self._StopOmniSharpServer( filepath )
-
-      if keeping_log_files:
-        ok_( os.path.exists( stdout_logfiles_location ),
-            "Logfile should still exist at "
-            "{0}".format( stdout_logfiles_location ) )
-        ok_( os.path.exists( stderr_logfiles_location ),
-            "Logfile should still exist at "
-            "{0}".format( stderr_logfiles_location ) )
-      else:
-        ok_( not os.path.exists( stdout_logfiles_location ),
-            "Logfile should no longer exist at "
-            "{0}".format( stdout_logfiles_location ) )
-        ok_( not os.path.exists( stderr_logfiles_location ),
-            "Logfile should no longer exist at "
-            "{0}".format( stderr_logfiles_location ) )
+          }
+        ]
+      }
+    ]
+  } )
+
+
+@IsolatedYcmd
+def Subcommands_StopServer_NoErrorIfNotStarted_test( app ):
+  filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+  StopOmniSharpServer( app, filepath )
+  # Success = no raise
+
+
+@IsolatedYcmd
+def StopServer_KeepLogFiles( app, keeping_log_files ):
+  with UserOption( 'server_keep_logfiles', keeping_log_files ):
+    filepath = PathToTestFile( 'testy', 'GotoTestCase.cs' )
+    contents = ReadFile( filepath )
+    event_data = BuildRequest( filepath = filepath,
+                               filetype = 'cs',
+                               contents = contents,
+                               event_name = 'FileReadyToParse' )
+
+    app.post_json( '/event_notification', event_data )
+    WaitUntilOmniSharpServerReady( app, filepath )
+
+    event_data = BuildRequest( filetype = 'cs', filepath = filepath )
+
+    debuginfo = app.post_json( '/debug_info', event_data ).json
+
+    log_files_match = re.search( "^OmniSharp logfiles:\n(.*)\n(.*)",
+                                 debuginfo,
+                                 re.MULTILINE )
+    stdout_logfiles_location = log_files_match.group( 1 )
+    stderr_logfiles_location = log_files_match.group( 2 )
+
+    try:
+      ok_( os.path.exists(stdout_logfiles_location ),
+           "Logfile should exist at {0}".format( stdout_logfiles_location ) )
+      ok_( os.path.exists( stderr_logfiles_location ),
+           "Logfile should exist at {0}".format( stderr_logfiles_location ) )
+    finally:
+      StopOmniSharpServer( app, filepath )
+
+    if keeping_log_files:
+      ok_( os.path.exists( stdout_logfiles_location ),
+           "Logfile should still exist at "
+           "{0}".format( stdout_logfiles_location ) )
+      ok_( os.path.exists( stderr_logfiles_location ),
+           "Logfile should still exist at "
+           "{0}".format( stderr_logfiles_location ) )
+    else:
+      ok_( not os.path.exists( stdout_logfiles_location ),
+           "Logfile should no longer exist at "
+           "{0}".format( stdout_logfiles_location ) )
+      ok_( not os.path.exists( stderr_logfiles_location ),
+           "Logfile should no longer exist at "
+           "{0}".format( stderr_logfiles_location ) )
+
+
+def Subcommands_StopServer_KeepLogFiles_test():
+  yield StopServer_KeepLogFiles, True
+  yield StopServer_KeepLogFiles, False
diff --git a/ycmd/tests/detailed_diagnostics_test.py b/ycmd/tests/detailed_diagnostics_test.py
deleted file mode 100644
--- a/ycmd/tests/detailed_diagnostics_test.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright (C) 2016 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from nose.tools import eq_
-from hamcrest import assert_that
-from ..responses import NoDiagnosticSupport, BuildDisplayMessageResponse
-from .handlers_test import Handlers_test
-from .test_utils import DummyCompleter
-from mock import patch
-import http.client
-
-
-class Diagnostics_test( Handlers_test ):
-
-  def DoesntWork_test( self ):
-    with self.PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
-      diag_data = self._BuildRequest( contents = "foo = 5",
-                                      line_num = 2,
-                                      filetype = 'dummy_filetype' )
-
-      response = self._app.post_json( '/detailed_diagnostic',
-                                      diag_data,
-                                      expect_errors = True )
-
-      eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-      assert_that( response.json, self._ErrorMatcher( NoDiagnosticSupport ) )
-
-
-  @patch( 'ycmd.tests.test_utils.DummyCompleter.GetDetailedDiagnostic',
-          return_value = BuildDisplayMessageResponse( "detailed diagnostic" ) )
-  def DoesWork_test( self, *args ):
-    with self.PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
-      diag_data = self._BuildRequest( contents = "foo = 5",
-                                      filetype = 'dummy_filetype' )
-
-      response = self._app.post_json( '/detailed_diagnostic', diag_data )
-      assert_that( response.json,
-                   self._MessageMatcher( "detailed diagnostic" ) )
diff --git a/ycmd/tests/diagnostics_test.py b/ycmd/tests/diagnostics_test.py
new file mode 100644
--- /dev/null
+++ b/ycmd/tests/diagnostics_test.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+from hamcrest import assert_that
+from mock import patch
+from nose.tools import eq_
+import http.client
+
+from ycmd.responses import NoDiagnosticSupport, BuildDisplayMessageResponse
+from ycmd.tests import SharedYcmd
+from ycmd.tests.test_utils import ( BuildRequest, DummyCompleter, ErrorMatcher,
+                                    MessageMatcher, PatchCompleter )
+
+
+@SharedYcmd
+def Diagnostics_DoesntWork_test( app ):
+  with PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
+    diag_data = BuildRequest( contents = "foo = 5",
+                              line_num = 2,
+                              filetype = 'dummy_filetype' )
+
+    response = app.post_json( '/detailed_diagnostic',
+                              diag_data,
+                              expect_errors = True )
+
+    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+    assert_that( response.json, ErrorMatcher( NoDiagnosticSupport ) )
+
+
+@SharedYcmd
+@patch( 'ycmd.tests.test_utils.DummyCompleter.GetDetailedDiagnostic',
+        return_value = BuildDisplayMessageResponse( 'detailed diagnostic' ) )
+def Diagnostics_DoesWork_test( app, *args ):
+  with PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
+    diag_data = BuildRequest( contents = 'foo = 5',
+                              filetype = 'dummy_filetype' )
+
+    response = app.post_json( '/detailed_diagnostic', diag_data )
+    assert_that( response.json, MessageMatcher( 'detailed diagnostic' ) )
diff --git a/ycmd/tests/get_completions_test.py b/ycmd/tests/get_completions_test.py
--- a/ycmd/tests/get_completions_test.py
+++ b/ycmd/tests/get_completions_test.py
@@ -24,171 +24,178 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from webtest import TestApp
-from nose.tools import eq_
 from hamcrest import assert_that, has_items
-from .. import handlers
-from .handlers_test import Handlers_test
-from ycmd.tests.test_utils import DummyCompleter
 from mock import patch
+from nose.tools import eq_
 
-
-class GetCompletions_test( Handlers_test ):
-
-  def RequestValidation_NoLineNumException_test( self ):
-    response = self._app.post_json( '/semantic_completion_available', {
-      'column_num': 0,
-      'filepath': '/foo',
-      'file_data': {
-        '/foo': {
-          'filetypes': [ 'text' ],
-          'contents': 'zoo'
-        }
+from ycmd.tests import SharedYcmd
+from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher,
+                                    DummyCompleter, PatchCompleter,
+                                    UserOption )
+
+
+@SharedYcmd
+def GetCompletions_RequestValidation_NoLineNumException_test( app ):
+  response = app.post_json( '/semantic_completion_available', {
+    'column_num': 0,
+    'filepath': '/foo',
+    'file_data': {
+      '/foo': {
+        'filetypes': [ 'text' ],
+        'contents': 'zoo'
       }
-    }, status = '5*', expect_errors = True )
-    response.mustcontain( 'missing', 'line_num' )
-
-
-  def IdentifierCompleter_Works_test( self ):
-    event_data = self._BuildRequest( contents = 'foo foogoo ba',
-                                     event_name = 'FileReadyToParse' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    # query is 'oo'
-    completion_data = self._BuildRequest( contents = 'oo foo foogoo ba',
-                                          column_num = 3 )
-    response_data = self._app.post_json( '/completions', completion_data ).json
-
-    eq_( 1, response_data[ 'completion_start_column' ] )
-    assert_that(
-      response_data[ 'completions' ],
-      has_items( self._CompletionEntryMatcher( 'foo', '[ID]' ),
-                 self._CompletionEntryMatcher( 'foogoo', '[ID]' ) )
-    )
-
-
-  def IdentifierCompleter_StartColumn_AfterWord_test( self ):
-    completion_data = self._BuildRequest( contents = 'oo foo foogoo ba',
-                                          column_num = 11 )
-    response_data = self._app.post_json( '/completions', completion_data ).json
-    eq_( 8, response_data[ 'completion_start_column' ] )
-
-
-  def IdentifierCompleter_WorksForSpecialIdentifierChars_test( self ):
-    contents = """
-      textarea {
-        font-family: sans-serif;
-        font-size: 12px;
-      }"""
-    event_data = self._BuildRequest( contents = contents,
-                                     filetype = 'css',
-                                     event_name = 'FileReadyToParse' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    # query is 'fo'
-    completion_data = self._BuildRequest( contents = 'fo ' + contents,
-                                          filetype = 'css',
-                                          column_num = 3 )
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-
-    assert_that(
-      results,
-      has_items( self._CompletionEntryMatcher( 'font-size', '[ID]' ),
-                 self._CompletionEntryMatcher( 'font-family', '[ID]' ) )
+    }
+  }, status = '5*', expect_errors = True )
+  response.mustcontain( 'missing', 'line_num' )
+
+
+@SharedYcmd
+def GetCompletions_IdentifierCompleter_Works_test( app ):
+  event_data = BuildRequest( contents = 'foo foogoo ba',
+                             event_name = 'FileReadyToParse' )
+
+  app.post_json( '/event_notification', event_data )
+
+  # query is 'oo'
+  completion_data = BuildRequest( contents = 'oo foo foogoo ba',
+                                  column_num = 3 )
+  response_data = app.post_json( '/completions', completion_data ).json
+
+  eq_( 1, response_data[ 'completion_start_column' ] )
+  assert_that(
+    response_data[ 'completions' ],
+    has_items( CompletionEntryMatcher( 'foo', '[ID]' ),
+               CompletionEntryMatcher( 'foogoo', '[ID]' ) )
+  )
+
+
+@SharedYcmd
+def GetCompletions_IdentifierCompleter_StartColumn_AfterWord_test( app ):
+  completion_data = BuildRequest( contents = 'oo foo foogoo ba',
+                                  column_num = 11 )
+  response_data = app.post_json( '/completions', completion_data ).json
+  eq_( 8, response_data[ 'completion_start_column' ] )
+
+
+@SharedYcmd
+def GetCompletions_IdentifierCompleter_WorksForSpecialIdentifierChars_test(
+  app ):
+  contents = """
+    textarea {
+      font-family: sans-serif;
+      font-size: 12px;
+    }"""
+  event_data = BuildRequest( contents = contents,
+                             filetype = 'css',
+                             event_name = 'FileReadyToParse' )
+
+  app.post_json( '/event_notification', event_data )
+
+  # query is 'fo'
+  completion_data = BuildRequest( contents = 'fo ' + contents,
+                                  filetype = 'css',
+                                  column_num = 3 )
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+
+  assert_that(
+    results,
+    has_items( CompletionEntryMatcher( 'font-size', '[ID]' ),
+               CompletionEntryMatcher( 'font-family', '[ID]' ) )
+  )
+
+
+@SharedYcmd
+@patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList',
+        return_value = [ 'foo', 'bar', 'qux' ] )
+def GetCompletions_ForceSemantic_Works_test( app, *args ):
+  with PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+    completion_data = BuildRequest( filetype = 'dummy_filetype',
+                                    force_semantic = True )
+
+    results = app.post_json( '/completions',
+                             completion_data ).json[ 'completions' ]
+    assert_that( results, has_items( CompletionEntryMatcher( 'foo' ),
+                                     CompletionEntryMatcher( 'bar' ),
+                                     CompletionEntryMatcher( 'qux' ) ) )
+
+
+@SharedYcmd
+def GetCompletions_IdentifierCompleter_SyntaxKeywordsAdded_test( app ):
+  event_data = BuildRequest( event_name = 'FileReadyToParse',
+                             syntax_keywords = ['foo', 'bar', 'zoo'] )
+
+  app.post_json( '/event_notification', event_data )
+
+  completion_data = BuildRequest( contents = 'oo ',
+                                  column_num = 3 )
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that( results,
+               has_items( CompletionEntryMatcher( 'foo' ),
+                          CompletionEntryMatcher( 'zoo' ) ) )
+
+
+@SharedYcmd
+def GetCompletions_UltiSnipsCompleter_Works_test( app ):
+  event_data = BuildRequest(
+    event_name = 'BufferVisit',
+    ultisnips_snippets = [
+        {'trigger': 'foo', 'description': 'bar'},
+        {'trigger': 'zoo', 'description': 'goo'},
+    ] )
+
+  app.post_json( '/event_notification', event_data )
+
+  completion_data = BuildRequest( contents = 'oo ',
+                                  column_num = 3 )
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that(
+    results,
+    has_items(
+      CompletionEntryMatcher( 'foo', extra_menu_info='<snip> bar' ),
+      CompletionEntryMatcher( 'zoo', extra_menu_info='<snip> goo' )
     )
+  )
 
 
-  @patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList',
-          return_value = [ 'foo', 'bar', 'qux' ] )
-  def ForceSemantic_Works_test( self, *args ):
-    with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
-      completion_data = self._BuildRequest( filetype = 'dummy_filetype',
-                                            force_semantic = True )
-
-      results = self._app.post_json( '/completions',
-                                     completion_data ).json[ 'completions' ]
-      assert_that( results, has_items( self._CompletionEntryMatcher( 'foo' ),
-                                       self._CompletionEntryMatcher( 'bar' ),
-                                       self._CompletionEntryMatcher( 'qux' ) ) )
-
-
-  def IdentifierCompleter_SyntaxKeywordsAdded_test( self ):
-    event_data = self._BuildRequest( event_name = 'FileReadyToParse',
-                                     syntax_keywords = ['foo', 'bar', 'zoo'] )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    completion_data = self._BuildRequest( contents = 'oo ',
-                                          column_num = 3 )
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that( results,
-                 has_items( self._CompletionEntryMatcher( 'foo' ),
-                            self._CompletionEntryMatcher( 'zoo' ) ) )
-
-
-  def UltiSnipsCompleter_Works_test( self ):
-    event_data = self._BuildRequest(
+@SharedYcmd
+def GetCompletions_UltiSnipsCompleter_UnusedWhenOffWithOption_test( app ):
+  with UserOption( 'use_ultisnips_completer', False ):
+    event_data = BuildRequest(
       event_name = 'BufferVisit',
       ultisnips_snippets = [
           {'trigger': 'foo', 'description': 'bar'},
           {'trigger': 'zoo', 'description': 'goo'},
       ] )
 
-    self._app.post_json( '/event_notification', event_data )
-
-    completion_data = self._BuildRequest( contents = 'oo ',
-                                          column_num = 3 )
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that(
-      results,
-      has_items(
-        self._CompletionEntryMatcher( 'foo', extra_menu_info='<snip> bar' ),
-        self._CompletionEntryMatcher( 'zoo', extra_menu_info='<snip> goo' )
-      )
-    )
-
-
-  def UltiSnipsCompleter_UnusedWhenOffWithOption_test( self ):
-    with self.UserOption( 'use_ultisnips_completer', False ):
-      self._app = TestApp( handlers.app )
+    app.post_json( '/event_notification', event_data )
 
-      event_data = self._BuildRequest(
-        event_name = 'BufferVisit',
-        ultisnips_snippets = [
-            {'trigger': 'foo', 'description': 'bar'},
-            {'trigger': 'zoo', 'description': 'goo'},
-        ] )
+    completion_data = BuildRequest( contents = 'oo ', column_num = 3 )
 
-      self._app.post_json( '/event_notification', event_data )
+    eq_( [],
+         app.post_json( '/completions',
+                        completion_data ).json[ 'completions' ] )
 
-      completion_data = self._BuildRequest( contents = 'oo ',
-                                            column_num = 3 )
 
-      eq_( [],
-           self._app.post_json( '/completions',
-                                completion_data ).json[ 'completions' ] )
+@SharedYcmd
+@patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList',
+        return_value = [ 'some_candidate' ] )
+def GetCompletions_SemanticCompleter_WorksWhenTriggerIsIdentifier_test(
+  app, *args ):
+  with UserOption( 'semantic_triggers',
+                   { 'dummy_filetype': [ '_' ] } ):
+    with PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+      completion_data = BuildRequest( filetype = 'dummy_filetype',
+                                      contents = 'some_can',
+                                      column_num = 9 )
 
-
-  @patch( 'ycmd.tests.test_utils.DummyCompleter.CandidatesList',
-          return_value = [ 'some_candidate' ] )
-  def SemanticCompleter_WorksWhenTriggerIsIdentifier_test( self, *args ):
-    with self.UserOption( 'semantic_triggers',
-                          { 'dummy_filetype': [ '_' ] } ):
-      with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
-        completion_data = self._BuildRequest( filetype = 'dummy_filetype',
-                                              contents = 'some_can',
-                                              column_num = 9 )
-
-        results = self._app.post_json( '/completions',
-                                       completion_data ).json[ 'completions' ]
-        assert_that(
-          results,
-          has_items( self._CompletionEntryMatcher( 'some_candidate' ) )
-        )
+      results = app.post_json( '/completions',
+                               completion_data ).json[ 'completions' ]
+      assert_that(
+        results,
+        has_items( CompletionEntryMatcher( 'some_candidate' ) )
+      )
diff --git a/ycmd/tests/go/__init__.py b/ycmd/tests/go/__init__.py
--- a/ycmd/tests/go/__init__.py
+++ b/ycmd/tests/go/__init__.py
@@ -0,0 +1,72 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+
+from ycmd.tests.test_utils import BuildRequest, SetUpApp
+
+shared_app = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def StopGoCodeServer( app ):
+  app.post_json( '/run_completer_command',
+                 BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'StopServer' ],
+                               filetype = 'go' ) )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+
+
+def tearDownPackage():
+  global shared_app
+
+  StopGoCodeServer( shared_app )
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
diff --git a/ycmd/tests/go/get_completions_test.py b/ycmd/tests/go/get_completions_test.py
--- a/ycmd/tests/go/get_completions_test.py
+++ b/ycmd/tests/go/get_completions_test.py
@@ -24,22 +24,23 @@
 from builtins import *  # noqa
 
 from hamcrest import assert_that, has_item
-from .go_handlers_test import Go_Handlers_test
-from ycmd.utils import ReadFile
 
+from ycmd.tests.go import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher
+from ycmd.utils import ReadFile
 
-class Go_GetCompletions_test( Go_Handlers_test ):
 
-  def Basic_test( self ):
-    filepath = self._PathToTestFile( 'test.go' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'go',
-                                          contents = ReadFile( filepath ),
-                                          force_semantic = True,
-                                          line_num = 9,
-                                          column_num = 11 )
+@SharedYcmd
+def GetCompletions_Basic_test( app ):
+  filepath = PathToTestFile( 'test.go' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'go',
+                                  contents = ReadFile( filepath ),
+                                  force_semantic = True,
+                                  line_num = 9,
+                                  column_num = 11 )
 
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that( results,
-                 has_item( self._CompletionEntryMatcher( u'Logger' ) ) )
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that( results,
+               has_item( CompletionEntryMatcher( u'Logger' ) ) )
diff --git a/ycmd/tests/go/go_handlers_test.py b/ycmd/tests/go/go_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/go/go_handlers_test.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from ..handlers_test import Handlers_test
-
-
-class Go_Handlers_test( Handlers_test ):
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def tearDown( self ):
-    self._StopGoCodeServer()
-
-
-  def _StopGoCodeServer( self ):
-    self._app.post_json(
-        '/run_completer_command',
-        self._BuildRequest( completer_target = 'filetype_default',
-                            command_arguments = [ 'StopServer' ],
-                            filetype = 'go' ) )
diff --git a/ycmd/tests/go/subcommands_test.py b/ycmd/tests/go/subcommands_test.py
--- a/ycmd/tests/go/subcommands_test.py
+++ b/ycmd/tests/go/subcommands_test.py
@@ -23,59 +23,60 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from .go_handlers_test import Go_Handlers_test
 from nose.tools import eq_
-from ycmd.utils import ReadFile
 
+from ycmd.tests.go import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest
+from ycmd.utils import ReadFile
 
-class Go_Subcommands_test( Go_Handlers_test ):
 
-  def _GoTo( self, params ):
-    filepath = self._PathToTestFile( 'goto.go' )
-    contents = ReadFile( filepath )
+@SharedYcmd
+def RunGoToTest( app, params ):
+  filepath = PathToTestFile( 'goto.go' )
+  contents = ReadFile( filepath )
 
-    command = params[ 'command' ]
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ command ],
-                                    line_num = 8,
-                                    column_num = 8,
-                                    contents = contents,
-                                    filetype = 'go',
-                                    filepath = filepath )
+  command = params[ 'command' ]
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ command ],
+                            line_num = 8,
+                            column_num = 8,
+                            contents = contents,
+                            filetype = 'go',
+                            filepath = filepath )
 
-    results = self._app.post_json( '/run_completer_command',
-                                   goto_data )
+  results = app.post_json( '/run_completer_command',
+                           goto_data )
 
-    eq_( {
-      'line_num': 3, 'column_num': 6, 'filepath': filepath
-    }, results.json )
+  eq_( {
+    'line_num': 3, 'column_num': 6, 'filepath': filepath
+  }, results.json )
 
-    filepath = self._PathToTestFile( 'win.go' )
-    contents = ReadFile( filepath )
+  filepath = PathToTestFile( 'win.go' )
+  contents = ReadFile( filepath )
 
-    command = params[ 'command' ]
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ command ],
-                                    line_num = 4,
-                                    column_num = 7,
-                                    contents = contents,
-                                    filetype = 'go',
-                                    filepath = filepath )
+  command = params[ 'command' ]
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ command ],
+                            line_num = 4,
+                            column_num = 7,
+                            contents = contents,
+                            filetype = 'go',
+                            filepath = filepath )
 
-    results = self._app.post_json( '/run_completer_command',
-                                   goto_data )
+  results = app.post_json( '/run_completer_command',
+                           goto_data )
 
-    eq_( {
-      'line_num': 2, 'column_num': 6, 'filepath': filepath
-    }, results.json )
+  eq_( {
+    'line_num': 2, 'column_num': 6, 'filepath': filepath
+  }, results.json )
 
 
-  def GoTo_all_test( self ):
-    tests = [
-      { 'command': 'GoTo' },
-      { 'command': 'GoToDefinition' },
-      { 'command': 'GoToDeclaration' }
-    ]
+def Subcommands_GoTo_all_test():
+  tests = [
+    { 'command': 'GoTo' },
+    { 'command': 'GoToDefinition' },
+    { 'command': 'GoToDeclaration' }
+  ]
 
-    for test in tests:
-      yield ( self._GoTo, test )
+  for test in tests:
+    yield RunGoToTest, test
diff --git a/ycmd/tests/handlers_test.py b/ycmd/tests/handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/handlers_test.py
+++ /dev/null
@@ -1,116 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import absolute_import
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from ..server_utils import SetUpPythonPath
-SetUpPythonPath()
-from webtest import TestApp
-from .. import handlers
-from ycmd import user_options_store
-from hamcrest import has_entries, has_entry, contains_string
-from .test_utils import BuildRequest
-from mock import patch
-import contextlib
-import bottle
-import os
-
-
-class Handlers_test( object ):
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def setUp( self ):
-    bottle.debug( True )
-    handlers.SetServerStateToDefaults()
-    self._app = TestApp( handlers.app )
-
-
-  @contextlib.contextmanager
-  def PatchCompleter( self, completer, filetype ):
-    user_options = handlers._server_state._user_options
-    with patch.dict( 'ycmd.handlers._server_state._filetype_completers',
-                     { filetype: completer( user_options ) } ):
-      yield
-
-
-  @contextlib.contextmanager
-  def UserOption( self, key, value ):
-    try:
-      current_options = dict( user_options_store.GetAll() )
-      user_options = current_options.copy()
-      user_options.update( { key: value } )
-      handlers.UpdateUserOptions( user_options )
-      yield
-    finally:
-      handlers.UpdateUserOptions( current_options )
-
-
-  @staticmethod
-  def _BuildRequest( **kwargs ):
-    return BuildRequest( **kwargs )
-
-
-  @staticmethod
-  def _CompletionEntryMatcher( insertion_text,
-                               extra_menu_info = None,
-                               extra_params = None ):
-    match = { 'insertion_text': insertion_text }
-
-    if extra_menu_info:
-      match.update( { 'extra_menu_info': extra_menu_info } )
-
-    if extra_params:
-      match.update( extra_params )
-
-    return has_entries( match )
-
-
-  @staticmethod
-  def _CompletionLocationMatcher( location_type, value ):
-    return has_entry( 'extra_data',
-                      has_entry( 'location',
-                                 has_entry( location_type, value ) ) )
-
-
-  @staticmethod
-  def _ErrorMatcher( cls, msg = None ):
-    """ Returns a hamcrest matcher for a server exception response """
-    entry = { 'exception': has_entry( 'TYPE', cls.__name__ ) }
-
-    if msg:
-      entry.update( { 'message': msg } )
-
-    return has_entries( entry )
-
-
-  @staticmethod
-  def _MessageMatcher( msg ):
-    return has_entry( 'message', contains_string( msg ) )
-
-
-  def _PathToTestFile( self, *args ):
-    dir_of_current_script = os.path.dirname( os.path.abspath( self._file ) )
-    return os.path.join( dir_of_current_script, 'testdata', *args )
diff --git a/ycmd/tests/javascript/__init__.py b/ycmd/tests/javascript/__init__.py
--- a/ycmd/tests/javascript/__init__.py
+++ b/ycmd/tests/javascript/__init__.py
@@ -0,0 +1,129 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+import time
+
+from ycmd import handlers
+from ycmd.tests.test_utils import BuildRequest, SetUpApp
+
+shared_app = None
+shared_current_dir = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def WaitUntilTernServerReady( app ):
+  app.post_json( '/run_completer_command', BuildRequest(
+    command_arguments = [ 'StartServer' ],
+    completer_target = 'filetype_default',
+    filetype = 'javascript',
+    filepath = '/foo.js',
+    contents = '',
+    line_num = '1'
+  ) )
+
+  retries = 100
+  while retries > 0:
+    result = app.get( '/ready', { 'subserver': 'javascript' } ).json
+    if result:
+      return
+
+    time.sleep( 0.2 )
+    retries = retries - 1
+
+  raise RuntimeError( 'Timeout waiting for Tern.js server to be ready' )
+
+
+def StopTernServer( app ):
+  app.post_json( '/run_completer_command',
+                 BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'StopServer' ],
+                               filetype = 'javascript' ),
+                 expect_errors = True )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app, shared_current_dir
+
+  shared_app = SetUpApp()
+  shared_current_dir = os.getcwd()
+  os.chdir( PathToTestFile() )
+  WaitUntilTernServerReady( shared_app )
+
+
+def tearDownPackage():
+  """Cleans up the tests using the SharedYcmd decorator in this package. It is
+  executed once after running all the tests in the package."""
+  global shared_app, shared_current_dir
+
+  StopTernServer( shared_app )
+  os.chdir( shared_current_dir )
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
+
+
+def IsolatedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes a unique ycmd application as a parameter. It should be used on tests
+  that change the server state in a irreversible way (ex: a semantic subserver
+  is stopped or restarted) or expect a clean state (ex: no semantic subserver
+  started, no .ycm_extra_conf.py loaded, etc).
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    old_server_state = handlers._server_state
+    old_current_dir = os.getcwd()
+
+    try:
+      os.chdir( PathToTestFile() )
+      app = SetUpApp()
+      WaitUntilTernServerReady( app )
+      test( app, *args, **kwargs )
+      StopTernServer( app )
+    finally:
+      os.chdir( old_current_dir )
+      handlers._server_state = old_server_state
+  return Wrapper
diff --git a/ycmd/tests/javascript/event_notification_test.py b/ycmd/tests/javascript/event_notification_test.py
--- a/ycmd/tests/javascript/event_notification_test.py
+++ b/ycmd/tests/javascript/event_notification_test.py
@@ -23,149 +23,146 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from ycmd.server_utils import SetUpPythonPath
-from ycmd.utils import ReadFile
-SetUpPythonPath()
-from nose.tools import eq_
 from hamcrest import assert_that, empty
-
-from .javascript_handlers_test import Javascript_Handlers_test
+from mock import patch
+from nose.tools import eq_
 from pprint import pformat
 import http.client
 import os
-from mock import patch
 
+from ycmd.tests.test_utils import BuildRequest, ErrorMatcher
+from ycmd.tests.javascript import ( IsolatedYcmd, PathToTestFile,
+                                    WaitUntilTernServerReady )
+from ycmd.utils import ReadFile
 
-class Javascript_EventNotification_test( Javascript_Handlers_test ):
-
-  def OnFileReadyToParse_ProjectFile_cwd_test( self ):
-    contents = ReadFile( self._PathToTestFile( 'simple_test.js' ) )
 
-    response = self._app.post_json( '/event_notification',
-                                    self._BuildRequest(
-                                      event_name = 'FileReadyToParse',
-                                      contents = contents,
-                                      filetype = 'javascript' ),
-                                    expect_errors = True)
-
-    eq_( response.status_code, http.client.OK )
-    assert_that( response.json, empty() )
-
-
-  def OnFileReadyToParse_ProjectFile_parentdir_test( self ):
-    os.chdir( self._PathToTestFile( 'lamelib' ) )
-    contents = ReadFile( self._PathToTestFile( 'simple_test.js' ) )
-
-    response = self._app.post_json( '/event_notification',
-                                    self._BuildRequest(
-                                      event_name = 'FileReadyToParse',
-                                      contents = contents,
-                                      filetype = 'javascript' ),
-                                    expect_errors = True)
-
-    eq_( response.status_code, http.client.OK )
-    assert_that( response.json, empty() )
-
-
-  @patch( 'ycmd.completers.javascript.tern_completer.GlobalConfigExists',
-          return_value = False )
-  def OnFileReadyToParse_NoProjectFile_test( self, *args ):
-    # We raise an error if we can't detect a .tern-project file.
-    # We only do this on the first OnFileReadyToParse event after a
-    # server startup.
-    os.chdir( self._PathToTestFile( '..' ) )
-    contents = ReadFile( self._PathToTestFile( 'simple_test.js' ) )
-
-    response = self._app.post_json( '/event_notification',
-                                    self._BuildRequest(
-                                      event_name = 'FileReadyToParse',
-                                      contents = contents,
-                                      filetype = 'javascript' ),
-                                    expect_errors = True )
-
-    print( 'event response: {0}'.format( pformat( response.json ) ) )
-
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-
-    assert_that(
-      response.json,
-      self._ErrorMatcher( RuntimeError,
-                          'Warning: Unable to detect a .tern-project file '
-                          'in the hierarchy before ' + os.getcwd() +
-                          ' and no global .tern-config file was found. '
-                          'This is required for accurate JavaScript '
-                          'completion. Please see the User Guide for '
-                          'details.' )
-    )
-
-    # Check that a subsequent call does *not* raise the error
-
-    response = self._app.post_json( '/event_notification',
-                                    self._BuildRequest(
-                                      event_name = 'FileReadyToParse',
-                                      contents = contents,
-                                      filetype = 'javascript' ),
-                                    expect_errors = True )
-
-    print( 'event response: {0}'.format( pformat( response.json ) ) )
-
-    eq_( response.status_code, http.client.OK )
-    assert_that( response.json, empty() )
-
-    # Restart the server and check that it raises it again
-
-    self._app.post_json(
-      '/run_completer_command',
-      self._BuildRequest( command_arguments = [ 'StopServer' ],
-                          filetype = 'javascript',
-                          contents = contents,
-                          completer_target = 'filetype_default' )
-    )
-    self._app.post_json(
-      '/run_completer_command',
-      self._BuildRequest( command_arguments = [ 'StartServer' ],
-                          filetype = 'javascript',
-                          contents = contents,
-                          completer_target = 'filetype_default' ) )
-
-    self._WaitUntilTernServerReady()
-
-    response = self._app.post_json( '/event_notification',
-                                    self._BuildRequest(
-                                      event_name = 'FileReadyToParse',
-                                      contents = contents,
-                                      filetype = 'javascript' ),
-                                    expect_errors = True)
-
-    print( 'event response: {0}'.format( pformat( response.json ) ) )
-
-    eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
-
-    assert_that(
-      response.json,
-      self._ErrorMatcher( RuntimeError,
-                          'Warning: Unable to detect a .tern-project file '
-                          'in the hierarchy before ' + os.getcwd() +
-                          ' and no global .tern-config file was found. '
-                          'This is required for accurate JavaScript '
-                          'completion. Please see the User Guide for '
-                          'details.' )
-    )
-
-
-  @patch( 'ycmd.completers.javascript.tern_completer.GlobalConfigExists',
-          return_value = True )
-  def OnFileReadyToParse_UseGlobalConfig_test( self, *args ):
-    os.chdir( self._PathToTestFile( '..' ) )
-    contents = ReadFile( self._PathToTestFile( 'simple_test.js' ) )
-
-    response = self._app.post_json( '/event_notification',
-                                    self._BuildRequest(
-                                      event_name = 'FileReadyToParse',
-                                      contents = contents,
-                                      filetype = 'javascript' ),
-                                    expect_errors = True )
-
-    print( 'event response: {0}'.format( pformat( response.json ) ) )
-
-    eq_( response.status_code, http.client.OK )
+@IsolatedYcmd
+def EventNotification_OnFileReadyToParse_ProjectFile_cwd_test( app ):
+  contents = ReadFile( PathToTestFile( 'simple_test.js' ) )
+
+  response = app.post_json( '/event_notification',
+                            BuildRequest(
+                              event_name = 'FileReadyToParse',
+                              contents = contents,
+                              filetype = 'javascript' ),
+                            expect_errors = True)
+
+  eq_( response.status_code, http.client.OK )
+  assert_that( response.json, empty() )
+
+
+@IsolatedYcmd
+def EventNotification_OnFileReadyToParse_ProjectFile_parentdir_test( app ):
+  os.chdir( PathToTestFile( 'lamelib' ) )
+  contents = ReadFile( PathToTestFile( 'simple_test.js' ) )
+
+  response = app.post_json( '/event_notification',
+                            BuildRequest(
+                              event_name = 'FileReadyToParse',
+                              contents = contents,
+                              filetype = 'javascript' ),
+                            expect_errors = True)
+
+  eq_( response.status_code, http.client.OK )
+  assert_that( response.json, empty() )
+
+
+@IsolatedYcmd
+@patch( 'ycmd.completers.javascript.tern_completer.GlobalConfigExists',
+        return_value = False )
+def EventNotification_OnFileReadyToParse_NoProjectFile_test( app, *args ):
+  # We raise an error if we can't detect a .tern-project file.
+  # We only do this on the first OnFileReadyToParse event after a
+  # server startup.
+  os.chdir( PathToTestFile( '..' ) )
+  contents = ReadFile( PathToTestFile( 'simple_test.js' ) )
+
+  response = app.post_json( '/event_notification',
+                            BuildRequest(
+                              event_name = 'FileReadyToParse',
+                              contents = contents,
+                              filetype = 'javascript' ),
+                            expect_errors = True )
+
+  print( 'event response: {0}'.format( pformat( response.json ) ) )
+
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+
+  assert_that(
+    response.json,
+    ErrorMatcher( RuntimeError,
+                  'Warning: Unable to detect a .tern-project file '
+                  'in the hierarchy before ' + os.getcwd() +
+                  ' and no global .tern-config file was found. '
+                  'This is required for accurate JavaScript '
+                  'completion. Please see the User Guide for '
+                  'details.' )
+  )
+
+  # Check that a subsequent call does *not* raise the error
+
+  response = app.post_json( '/event_notification',
+                            BuildRequest(
+                              event_name = 'FileReadyToParse',
+                              contents = contents,
+                              filetype = 'javascript' ),
+                            expect_errors = True )
+
+  print( 'event response: {0}'.format( pformat( response.json ) ) )
+
+  eq_( response.status_code, http.client.OK )
+  assert_that( response.json, empty() )
+
+  # Restart the server and check that it raises it again
+
+  app.post_json( '/run_completer_command',
+                 BuildRequest( command_arguments = [ 'StopServer' ],
+                               filetype = 'javascript',
+                               contents = contents,
+                               completer_target = 'filetype_default' ) )
+  app.post_json( '/run_completer_command',
+                 BuildRequest( command_arguments = [ 'StartServer' ],
+                               filetype = 'javascript',
+                               contents = contents,
+                               completer_target = 'filetype_default' ) )
+
+  WaitUntilTernServerReady( app )
+
+  response = app.post_json( '/event_notification',
+                            BuildRequest( event_name = 'FileReadyToParse',
+                                          contents = contents,
+                                          filetype = 'javascript' ),
+                            expect_errors = True )
+
+  print( 'event response: {0}'.format( pformat( response.json ) ) )
+
+  eq_( response.status_code, http.client.INTERNAL_SERVER_ERROR )
+
+  assert_that(
+    response.json,
+    ErrorMatcher( RuntimeError,
+                  'Warning: Unable to detect a .tern-project file '
+                  'in the hierarchy before ' + os.getcwd() +
+                  ' and no global .tern-config file was found. '
+                  'This is required for accurate JavaScript '
+                  'completion. Please see the User Guide for '
+                  'details.' )
+  )
+
+
+@IsolatedYcmd
+@patch( 'ycmd.completers.javascript.tern_completer.GlobalConfigExists',
+        return_value = True )
+def EventNotification_OnFileReadyToParse_UseGlobalConfig_test( app, *args ):
+  os.chdir( PathToTestFile( '..' ) )
+  contents = ReadFile( PathToTestFile( 'simple_test.js' ) )
+
+  response = app.post_json( '/event_notification',
+                            BuildRequest( event_name = 'FileReadyToParse',
+                                          contents = contents,
+                                          filetype = 'javascript' ),
+                            expect_errors = True )
+
+  print( 'event response: {0}'.format( pformat( response.json ) ) )
+
+  eq_( response.status_code, http.client.OK )
diff --git a/ycmd/tests/javascript/get_completions_test.py b/ycmd/tests/javascript/get_completions_test.py
--- a/ycmd/tests/javascript/get_completions_test.py
+++ b/ycmd/tests/javascript/get_completions_test.py
@@ -23,14 +23,16 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from nose.tools import eq_
 from hamcrest import ( assert_that, contains, contains_inanyorder, empty,
                        has_entries )
-from .javascript_handlers_test import Javascript_Handlers_test
+from nose.tools import eq_
 from pprint import pformat
-from ycmd.utils import ReadFile
 import http.client
 
+from ycmd.tests.javascript import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher
+from ycmd.utils import ReadFile
+
 # The following properties/methods are in Object.prototype, so are present
 # on all objects:
 #
@@ -42,259 +44,264 @@
 # isPrototypeOf()
 
 
-class Javascript_GetCompletions_test( Javascript_Handlers_test ):
+def RunTest( app, test ):
+  """
+  Method to run a simple completion test and verify the result
 
-  def _RunTest( self, test ):
-    """
-    Method to run a simple completion test and verify the result
+  test is a dictionary containing:
+    'request': kwargs for BuildRequest
+    'expect': {
+       'response': server response code (e.g. httplib.OK)
+       'data': matcher for the server response json
+    }
+  """
 
-    test is a dictionary containing:
-      'request': kwargs for BuildRequest
-      'expect': {
-         'response': server response code (e.g. httplib.OK)
-         'data': matcher for the server response json
-      }
-    """
+  contents = ReadFile( test[ 'request' ][ 'filepath' ] )
 
-    contents = ReadFile( test[ 'request' ][ 'filepath' ] )
+  def CombineRequest( request, data ):
+    kw = request
+    request.update( data )
+    return BuildRequest( **kw )
 
-    def CombineRequest( request, data ):
-      kw = request
-      request.update( data )
-      return self._BuildRequest( **kw )
+  app.post_json( '/event_notification',
+                 CombineRequest( test[ 'request' ], {
+                                 'event_name': 'FileReadyToParse',
+                                 'contents': contents,
+                                 } ),
+                 expect_errors = True )
 
-    self._app.post_json( '/event_notification',
-                         CombineRequest( test[ 'request' ], {
-                                         'event_name': 'FileReadyToParse',
-                                         'contents': contents,
-                                         } ),
-                         expect_errors = True )
+  # We ignore errors here and we check the response code ourself.
+  # This is to allow testing of requests returning errors.
+  response = app.post_json( '/completions',
+                            CombineRequest( test[ 'request' ], {
+                              'contents': contents
+                            } ),
+                            expect_errors = True )
 
-    # We ignore errors here and we check the response code ourself.
-    # This is to allow testing of requests returning errors.
-    response = self._app.post_json( '/completions',
-                                    CombineRequest( test[ 'request' ], {
-                                      'contents': contents
-                                    } ),
-                                    expect_errors = True )
+  print( 'completer response: {0}'.format( pformat( response.json ) ) )
 
-    print( 'completer response: {0}'.format( pformat( response.json ) ) )
+  eq_( response.status_code, test[ 'expect' ][ 'response' ] )
 
-    eq_( response.status_code, test[ 'expect' ][ 'response' ] )
+  assert_that( response.json, test[ 'expect' ][ 'data' ] )
 
-    assert_that( response.json, test[ 'expect' ][ 'data' ] )
 
+@SharedYcmd
+def GetCompletions_NoQuery_test( app ):
+  RunTest( app, {
+    'description': 'semantic completion works for simple object no query',
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'simple_test.js' ),
+      'line_num'  : 13,
+      'column_num': 43,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          CompletionEntryMatcher( 'a_simple_function',
+                                  'fn(param: ?) -> string' ),
+          CompletionEntryMatcher( 'basic_type', 'number' ),
+          CompletionEntryMatcher( 'object', 'object' ),
+          CompletionEntryMatcher( 'toString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
+          CompletionEntryMatcher( 'hasOwnProperty',
+                                  'fn(prop: string) -> bool' ),
+          CompletionEntryMatcher( 'isPrototypeOf',
+                                  'fn(obj: ?) -> bool' ),
+          CompletionEntryMatcher( 'propertyIsEnumerable',
+                                  'fn(prop: string) -> bool' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
 
-  def NoQuery_test( self ):
-    self._RunTest( {
-      'description': 'semantic completion works for simple object no query',
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'simple_test.js' ),
-        'line_num'  : 13,
-        'column_num': 43,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            self._CompletionEntryMatcher( 'a_simple_function',
-                                          'fn(param: ?) -> string' ),
-            self._CompletionEntryMatcher( 'basic_type', 'number' ),
-            self._CompletionEntryMatcher( 'object', 'object' ),
-            self._CompletionEntryMatcher( 'toString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
-            self._CompletionEntryMatcher( 'hasOwnProperty',
-                                          'fn(prop: string) -> bool' ),
-            self._CompletionEntryMatcher( 'isPrototypeOf',
-                                          'fn(obj: ?) -> bool' ),
-            self._CompletionEntryMatcher( 'propertyIsEnumerable',
-                                          'fn(prop: string) -> bool' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
 
+@SharedYcmd
+def GetCompletions_Query_test( app ):
+  RunTest( app, {
+    'description': 'semantic completion works for simple object with query',
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'simple_test.js' ),
+      'line_num'  : 14,
+      'column_num': 45,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains(
+          CompletionEntryMatcher( 'basic_type', 'number' ),
+          CompletionEntryMatcher( 'isPrototypeOf',
+                                  'fn(obj: ?) -> bool' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
 
-  def Query_test( self ):
-    self._RunTest( {
-      'description': 'semantic completion works for simple object with query',
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'simple_test.js' ),
-        'line_num'  : 14,
-        'column_num': 45,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains(
-            self._CompletionEntryMatcher( 'basic_type', 'number' ),
-            self._CompletionEntryMatcher( 'isPrototypeOf',
-                                          'fn(obj: ?) -> bool' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
 
+@SharedYcmd
+def GetCompletions_Require_NoQuery_test( app ):
+  RunTest( app, {
+    'description': 'semantic completion works for simple object no query',
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'requirejs_test.js' ),
+      'line_num'  : 2,
+      'column_num': 15,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          CompletionEntryMatcher( 'mine_bitcoin',
+                                  'fn(how_much: ?) -> number' ),
+          CompletionEntryMatcher( 'get_number', 'number' ),
+          CompletionEntryMatcher( 'get_string', 'string' ),
+          CompletionEntryMatcher( 'get_thing',
+                                  'fn(a: ?) -> number|string' ),
+          CompletionEntryMatcher( 'toString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
+          CompletionEntryMatcher( 'hasOwnProperty',
+                                  'fn(prop: string) -> bool' ),
+          CompletionEntryMatcher( 'isPrototypeOf',
+                                  'fn(obj: ?) -> bool' ),
+          CompletionEntryMatcher( 'propertyIsEnumerable',
+                                  'fn(prop: string) -> bool' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
 
-  def Require_NoQuery_test( self ):
-    self._RunTest( {
-      'description': 'semantic completion works for simple object no query',
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'requirejs_test.js' ),
-        'line_num'  : 2,
-        'column_num': 15,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            self._CompletionEntryMatcher( 'mine_bitcoin',
-                                          'fn(how_much: ?) -> number' ),
-            self._CompletionEntryMatcher( 'get_number', 'number' ),
-            self._CompletionEntryMatcher( 'get_string', 'string' ),
-            self._CompletionEntryMatcher( 'get_thing',
-                                          'fn(a: ?) -> number|string' ),
-            self._CompletionEntryMatcher( 'toString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
-            self._CompletionEntryMatcher( 'hasOwnProperty',
-                                          'fn(prop: string) -> bool' ),
-            self._CompletionEntryMatcher( 'isPrototypeOf',
-                                          'fn(obj: ?) -> bool' ),
-            self._CompletionEntryMatcher( 'propertyIsEnumerable',
-                                          'fn(prop: string) -> bool' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
 
+@SharedYcmd
+def GetCompletions_Require_Query_test( app ):
+  RunTest( app, {
+    'description': 'semantic completion works for require object with query',
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'requirejs_test.js' ),
+      'line_num'  : 3,
+      'column_num': 17,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains(
+          CompletionEntryMatcher( 'mine_bitcoin',
+                                  'fn(how_much: ?) -> number' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
 
-  def Require_Query_test( self ):
-    self._RunTest( {
-      'description': 'semantic completion works for require object with query',
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'requirejs_test.js' ),
-        'line_num'  : 3,
-        'column_num': 17,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains(
-            self._CompletionEntryMatcher( 'mine_bitcoin',
-                                          'fn(how_much: ?) -> number' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
 
+@SharedYcmd
+def GetCompletions_Require_Query_LCS_test( app ):
+  RunTest( app, {
+    'description': ( 'completion works for require object '
+                     'with query not prefix' ),
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'requirejs_test.js' ),
+      'line_num'  : 4,
+      'column_num': 17,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains(
+          CompletionEntryMatcher( 'get_number', 'number' ),
+          CompletionEntryMatcher( 'get_thing',
+                                  'fn(a: ?) -> number|string' ),
+          CompletionEntryMatcher( 'get_string', 'string' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
 
-  def Require_Query_LCS_test( self ):
-    self._RunTest( {
-      'description': ( 'completion works for require object '
-                       'with query not prefix' ),
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'requirejs_test.js' ),
-        'line_num'  : 4,
-        'column_num': 17,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains(
-            self._CompletionEntryMatcher( 'get_number', 'number' ),
-            self._CompletionEntryMatcher( 'get_thing',
-                                          'fn(a: ?) -> number|string' ),
-            self._CompletionEntryMatcher( 'get_string', 'string' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
 
-
-  def DirtyNamedBuffers_test( self ):
-    # This tests that when we have dirty buffers in our editor, tern actually
-    # uses them correctly
-    self._RunTest( {
-      'description': ( 'completion works for require object '
-                       'with query not prefix' ),
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'requirejs_test.js' ),
-        'line_num'  : 18,
-        'column_num': 11,
-        'file_data': {
-          self._PathToTestFile( 'no_such_lib', 'no_such_file.js' ): {
-            'contents': (
-              'define( [], function() { return { big_endian_node: 1 } } )' ),
-            'filetypes': [ 'javascript' ]
-          }
-        },
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            self._CompletionEntryMatcher( 'big_endian_node', 'number' ),
-            self._CompletionEntryMatcher( 'toString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
-            self._CompletionEntryMatcher( 'hasOwnProperty',
-                                          'fn(prop: string) -> bool' ),
-            self._CompletionEntryMatcher( 'isPrototypeOf',
-                                          'fn(obj: ?) -> bool' ),
-            self._CompletionEntryMatcher( 'propertyIsEnumerable',
-                                          'fn(prop: string) -> bool' ),
-          ),
-          'errors': empty(),
-        } )
+@SharedYcmd
+def GetCompletions_DirtyNamedBuffers_test( app ):
+  # This tests that when we have dirty buffers in our editor, tern actually
+  # uses them correctly
+  RunTest( app, {
+    'description': ( 'completion works for require object '
+                     'with query not prefix' ),
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'requirejs_test.js' ),
+      'line_num'  : 18,
+      'column_num': 11,
+      'file_data': {
+        PathToTestFile( 'no_such_lib', 'no_such_file.js' ): {
+          'contents': (
+            'define( [], function() { return { big_endian_node: 1 } } )' ),
+          'filetypes': [ 'javascript' ]
+        }
       },
-    } )
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          CompletionEntryMatcher( 'big_endian_node', 'number' ),
+          CompletionEntryMatcher( 'toString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
+          CompletionEntryMatcher( 'hasOwnProperty',
+                                  'fn(prop: string) -> bool' ),
+          CompletionEntryMatcher( 'isPrototypeOf',
+                                  'fn(obj: ?) -> bool' ),
+          CompletionEntryMatcher( 'propertyIsEnumerable',
+                                  'fn(prop: string) -> bool' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
 
 
-  def ReturnsDocsInCompletions_test( self ):
-    # This tests that we supply docs for completions
-    self._RunTest( {
-      'description': 'completions supply docs',
-      'request': {
-        'filetype'  : 'javascript',
-        'filepath'  : self._PathToTestFile( 'requirejs_test.js' ),
-        'line_num'  : 8,
-        'column_num': 15,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            self._CompletionEntryMatcher(
-              'a_function',
-              'fn(bar: ?) -> {a_value: string}', {
-                'detailed_info': ( 'fn(bar: ?) -> {a_value: string}\n'
-                                   'This is a short documentation string'),
-              } ),
-            self._CompletionEntryMatcher( 'options', 'options' ),
-            self._CompletionEntryMatcher( 'toString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
-            self._CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
-            self._CompletionEntryMatcher( 'hasOwnProperty',
-                                          'fn(prop: string) -> bool' ),
-            self._CompletionEntryMatcher( 'isPrototypeOf',
-                                          'fn(obj: ?) -> bool' ),
-            self._CompletionEntryMatcher( 'propertyIsEnumerable',
-                                          'fn(prop: string) -> bool' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
+@SharedYcmd
+def GetCompletions_ReturnsDocsInCompletions_test( app ):
+  # This tests that we supply docs for completions
+  RunTest( app, {
+    'description': 'completions supply docs',
+    'request': {
+      'filetype'  : 'javascript',
+      'filepath'  : PathToTestFile( 'requirejs_test.js' ),
+      'line_num'  : 8,
+      'column_num': 15,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          CompletionEntryMatcher(
+            'a_function',
+            'fn(bar: ?) -> {a_value: string}', {
+              'detailed_info': ( 'fn(bar: ?) -> {a_value: string}\n'
+                                 'This is a short documentation string'),
+            } ),
+          CompletionEntryMatcher( 'options', 'options' ),
+          CompletionEntryMatcher( 'toString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'toLocaleString', 'fn() -> string' ),
+          CompletionEntryMatcher( 'valueOf', 'fn() -> number' ),
+          CompletionEntryMatcher( 'hasOwnProperty',
+                                  'fn(prop: string) -> bool' ),
+          CompletionEntryMatcher( 'isPrototypeOf',
+                                  'fn(obj: ?) -> bool' ),
+          CompletionEntryMatcher( 'propertyIsEnumerable',
+                                  'fn(prop: string) -> bool' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
diff --git a/ycmd/tests/javascript/javascript_handlers_test.py b/ycmd/tests/javascript/javascript_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/javascript/javascript_handlers_test.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from ..handlers_test import Handlers_test
-import os
-import time
-
-
-class Javascript_Handlers_test( Handlers_test ):
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def setUp( self ):
-    super( Javascript_Handlers_test, self ).setUp()
-
-    self._prev_current_dir = os.getcwd()
-    os.chdir( self._PathToTestFile() )
-
-    self._WaitUntilTernServerReady()
-
-
-  def tearDown( self ):
-    self._StopTernServer()
-
-    os.chdir( self._prev_current_dir )
-
-
-  def _StopTernServer( self ):
-    try:
-      self._app.post_json(
-        '/run_completer_command',
-        self._BuildRequest( command_arguments = [ 'StopServer' ],
-                            filetype = 'javascript',
-                            completer_target = 'filetype_default' )
-      )
-    except:
-      pass
-
-
-  def _WaitUntilTernServerReady( self ):
-    self._app.post_json( '/run_completer_command', self._BuildRequest(
-      command_arguments = [ 'StartServer' ],
-      completer_target = 'filetype_default',
-      filetype = 'javascript',
-      filepath = '/foo.js',
-      contents = '',
-      line_num = '1'
-    ) )
-
-    retries = 100
-    while retries > 0:
-      result = self._app.get( '/ready', { 'subserver': 'javascript' } ).json
-      if result:
-        return
-
-      time.sleep( 0.2 )
-      retries = retries - 1
-
-    raise RuntimeError( 'Timeout waiting for Tern.js server to be ready' )
diff --git a/ycmd/tests/javascript/subcommands_test.py b/ycmd/tests/javascript/subcommands_test.py
--- a/ycmd/tests/javascript/subcommands_test.py
+++ b/ycmd/tests/javascript/subcommands_test.py
@@ -23,16 +23,15 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
+from hamcrest import assert_that, contains, contains_inanyorder, has_entries
 from nose.tools import eq_
-from hamcrest import ( assert_that,
-                       contains,
-                       contains_inanyorder,
-                       has_entries )
-from .javascript_handlers_test import Javascript_Handlers_test
-from ycmd.utils import ReadFile
 from pprint import pformat
 import http.client
 
+from ycmd.tests.javascript import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, ErrorMatcher
+from ycmd.utils import ReadFile
+
 
 def LocationMatcher( filepath, column_num, line_num ):
   return has_entries( {
@@ -52,355 +51,362 @@ def ChunkMatcher( replacement_text, start, end ):
   } )
 
 
-class Javascript_Subcommands_test( Javascript_Handlers_test ):
-
-  def _RunTest( self, test ):
-    contents = ReadFile( test[ 'request' ][ 'filepath' ] )
-
-    def CombineRequest( request, data ):
-      kw = request
-      request.update( data )
-      return self._BuildRequest( **kw )
-
-    # Because we aren't testing this command, we *always* ignore errors. This
-    # is mainly because we (may) want to test scenarios where the completer
-    # throws an exception and the easiest way to do that is to throw from
-    # within the FlagsForFile function.
-    self._app.post_json( '/event_notification',
-                         CombineRequest( test[ 'request' ], {
-                                         'event_name': 'FileReadyToParse',
-                                         'contents': contents,
-                                         } ),
-                         expect_errors = True )
-
-    # We also ignore errors here, but then we check the response code
-    # ourself. This is to allow testing of requests returning errors.
-    response = self._app.post_json(
-      '/run_completer_command',
-      CombineRequest( test[ 'request' ], {
-        'completer_target': 'filetype_default',
-        'contents': contents,
-        'filetype': 'javascript',
-        'command_arguments': ( [ test[ 'request' ][ 'command' ] ]
-                               + test[ 'request' ].get( 'arguments', [] ) )
-      } ),
-      expect_errors = True
-    )
-
-    print( 'completer response: {0}'.format( pformat( response.json ) ) )
-
-    eq_( response.status_code, test[ 'expect' ][ 'response' ] )
-
-    assert_that( response.json, test[ 'expect' ][ 'data' ] )
-
-
-  def DefinedSubcommands_test( self ):
-    subcommands_data = self._BuildRequest( completer_target = 'javascript' )
-
-    self._WaitUntilTernServerReady()
-
-    eq_( sorted( [ 'GoToDefinition',
-                   'GoTo',
-                   'GetDoc',
-                   'GetType',
-                   'StartServer',
-                   'StopServer',
-                   'GoToReferences',
-                   'RefactorRename' ] ),
-         self._app.post_json( '/defined_subcommands',
-                              subcommands_data ).json )
-
-
-  def GoToDefinition_test( self ):
-    self._RunTest( {
-      'description': 'GoToDefinition works within file',
-      'request': {
-        'command': 'GoToDefinition',
-        'line_num': 13,
-        'column_num': 25,
-        'filepath': self._PathToTestFile( 'simple_test.js' ),
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'filepath': self._PathToTestFile( 'simple_test.js' ),
-          'line_num': 1,
-          'column_num': 5,
-        } )
-      }
-    } )
+@SharedYcmd
+def Subcommands_DefinedSubcommands_test( app ):
+  subcommands_data = BuildRequest( completer_target = 'javascript' )
+
+  eq_( sorted( [ 'GoToDefinition',
+                 'GoTo',
+                 'GetDoc',
+                 'GetType',
+                 'StartServer',
+                 'StopServer',
+                 'GoToReferences',
+                 'RefactorRename' ] ),
+       app.post_json( '/defined_subcommands',
+                      subcommands_data ).json )
+
+
+def RunTest( app, test ):
+  contents = ReadFile( test[ 'request' ][ 'filepath' ] )
+
+  def CombineRequest( request, data ):
+    kw = request
+    request.update( data )
+    return BuildRequest( **kw )
+
+  # Because we aren't testing this command, we *always* ignore errors. This
+  # is mainly because we (may) want to test scenarios where the completer
+  # throws an exception and the easiest way to do that is to throw from
+  # within the FlagsForFile function.
+  app.post_json( '/event_notification',
+                 CombineRequest( test[ 'request' ], {
+                                 'event_name': 'FileReadyToParse',
+                                 'contents': contents,
+                                 } ),
+                 expect_errors = True )
+
+  # We also ignore errors here, but then we check the response code
+  # ourself. This is to allow testing of requests returning errors.
+  response = app.post_json(
+    '/run_completer_command',
+    CombineRequest( test[ 'request' ], {
+      'completer_target': 'filetype_default',
+      'contents': contents,
+      'filetype': 'javascript',
+      'command_arguments': ( [ test[ 'request' ][ 'command' ] ]
+                             + test[ 'request' ].get( 'arguments', [] ) )
+    } ),
+    expect_errors = True
+  )
+
+  print( 'completer response: {0}'.format( pformat( response.json ) ) )
+
+  eq_( response.status_code, test[ 'expect' ][ 'response' ] )
+
+  assert_that( response.json, test[ 'expect' ][ 'data' ] )
+
+
+@SharedYcmd
+def Subcommands_GoToDefinition_test( app ):
+  RunTest( app, {
+    'description': 'GoToDefinition works within file',
+    'request': {
+      'command': 'GoToDefinition',
+      'line_num': 13,
+      'column_num': 25,
+      'filepath': PathToTestFile( 'simple_test.js' ),
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'filepath': PathToTestFile( 'simple_test.js' ),
+        'line_num': 1,
+        'column_num': 5,
+      } )
+    }
+  } )
 
 
-  def GoTo_test( self ):
-    self._RunTest( {
-      'description': 'GoTo works the same as GoToDefinition within file',
-      'request': {
-        'command': 'GoTo',
-        'line_num': 13,
-        'column_num': 25,
-        'filepath': self._PathToTestFile( 'simple_test.js' ),
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'filepath': self._PathToTestFile( 'simple_test.js' ),
-          'line_num': 1,
-          'column_num': 5,
-        } )
-      }
-    } )
+@SharedYcmd
+def Subcommands_GoTo_test( app ):
+  RunTest( app, {
+    'description': 'GoTo works the same as GoToDefinition within file',
+    'request': {
+      'command': 'GoTo',
+      'line_num': 13,
+      'column_num': 25,
+      'filepath': PathToTestFile( 'simple_test.js' ),
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'filepath': PathToTestFile( 'simple_test.js' ),
+        'line_num': 1,
+        'column_num': 5,
+      } )
+    }
+  } )
 
 
-  def GetDoc_test( self ):
-    self._RunTest( {
-      'description': 'GetDoc works within file',
-      'request': {
-        'command': 'GetDoc',
-        'line_num': 7,
-        'column_num': 16,
-        'filepath': self._PathToTestFile( 'coollib', 'cool_object.js' ),
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'detailed_info': (
-            'Name: mine_bitcoin\n'
-            'Type: fn(how_much: ?) -> number\n\n'
-            'This function takes a number and invests it in bitcoin. It '
-            'returns\nthe expected value (in notional currency) after 1 year.'
-          )
-        } )
-      }
-    } )
+@SharedYcmd
+def Subcommands_GetDoc_test( app ):
+  RunTest( app, {
+    'description': 'GetDoc works within file',
+    'request': {
+      'command': 'GetDoc',
+      'line_num': 7,
+      'column_num': 16,
+      'filepath': PathToTestFile( 'coollib', 'cool_object.js' ),
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'detailed_info': (
+          'Name: mine_bitcoin\n'
+          'Type: fn(how_much: ?) -> number\n\n'
+          'This function takes a number and invests it in bitcoin. It '
+          'returns\nthe expected value (in notional currency) after 1 year.'
+        )
+      } )
+    }
+  } )
 
 
-  def GetType_test( self ):
-    self._RunTest( {
-      'description': 'GetType works within file',
-      'request': {
-        'command': 'GetType',
-        'line_num': 11,
-        'column_num': 14,
-        'filepath': self._PathToTestFile( 'coollib', 'cool_object.js' ),
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'message': 'number'
-        } )
-      }
-    } )
+@SharedYcmd
+def Subcommands_GetType_test( app ):
+  RunTest( app, {
+    'description': 'GetType works within file',
+    'request': {
+      'command': 'GetType',
+      'line_num': 11,
+      'column_num': 14,
+      'filepath': PathToTestFile( 'coollib', 'cool_object.js' ),
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'message': 'number'
+      } )
+    }
+  } )
 
 
-  def GoToReferences_test( self ):
-    self._RunTest( {
-      'description': 'GoToReferences works within file',
-      'request': {
-        'command': 'GoToReferences',
-        'line_num': 17,
-        'column_num': 29,
-        'filepath': self._PathToTestFile( 'coollib', 'cool_object.js' ),
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': contains_inanyorder(
-          has_entries( {
-            'filepath': self._PathToTestFile( 'coollib', 'cool_object.js' ),
-            'line_num':  17,
-            'column_num': 29,
-          } ),
-          has_entries( {
-            'filepath': self._PathToTestFile( 'coollib', 'cool_object.js' ),
-            'line_num': 12,
-            'column_num': 9,
-          } )
-        )
-      }
-    } )
+@SharedYcmd
+def Subcommands_GoToReferences_test( app ):
+  RunTest( app, {
+    'description': 'GoToReferences works within file',
+    'request': {
+      'command': 'GoToReferences',
+      'line_num': 17,
+      'column_num': 29,
+      'filepath': PathToTestFile( 'coollib', 'cool_object.js' ),
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': contains_inanyorder(
+        has_entries( {
+          'filepath': PathToTestFile( 'coollib', 'cool_object.js' ),
+          'line_num': 17,
+          'column_num': 29,
+        } ),
+        has_entries( {
+          'filepath': PathToTestFile( 'coollib', 'cool_object.js' ),
+          'line_num': 12,
+          'column_num': 9,
+        } )
+      )
+    }
+  } )
 
 
-  def GetDocWithNoItendifier_test( self ):
-    self._RunTest( {
-      'description': 'GetDoc works when no identifier',
-      'request': {
-        'command': 'GetDoc',
-        'filepath': self._PathToTestFile( 'simple_test.js' ),
-        'line_num': 12,
-        'column_num': 1,
-      },
-      'expect': {
-        'response': http.client.INTERNAL_SERVER_ERROR,
-        'data': self._ErrorMatcher( RuntimeError, 'TernError: No type found '
-                                                  'at the given position.' ),
-      }
-    } )
+@SharedYcmd
+def Subcommands_GetDocWithNoItendifier_test( app ):
+  RunTest( app, {
+    'description': 'GetDoc works when no identifier',
+    'request': {
+      'command': 'GetDoc',
+      'filepath': PathToTestFile( 'simple_test.js' ),
+      'line_num': 12,
+      'column_num': 1,
+    },
+    'expect': {
+      'response': http.client.INTERNAL_SERVER_ERROR,
+      'data': ErrorMatcher( RuntimeError, 'TernError: No type found '
+                                          'at the given position.' ),
+    }
+  } )
 
 
-  def RefactorRename_Simple_test( self ):
-    filepath = self._PathToTestFile( 'simple_test.js' )
-    self._RunTest( {
-      'description': 'RefactorRename works within a single scope/file',
-      'request': {
-        'command': 'RefactorRename',
-        'arguments': [ 'test' ],
-        'filepath': filepath,
-        'line_num': 15,
-        'column_num': 32,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': {
-          'fixits': contains( has_entries( {
-            'chunks': contains(
-                ChunkMatcher( 'test',
-                              LocationMatcher( filepath, 1, 5 ),
-                              LocationMatcher( filepath, 1, 22 ) ),
-                ChunkMatcher( 'test',
-                              LocationMatcher( filepath, 13, 25 ),
-                              LocationMatcher( filepath, 13, 42 ) ),
-                ChunkMatcher( 'test',
-                              LocationMatcher( filepath, 14, 24 ),
-                              LocationMatcher( filepath, 14, 41 ) ),
-                ChunkMatcher( 'test',
-                              LocationMatcher( filepath, 15, 24 ),
-                              LocationMatcher( filepath, 15, 41 ) ),
-                ChunkMatcher( 'test',
-                              LocationMatcher( filepath, 21, 7 ),
-                              LocationMatcher( filepath, 21, 24 ) ),
-                # On the same line, ensuring offsets are as expected (as
-                # unmodified source, similar to clang)
-                ChunkMatcher( 'test',
-                              LocationMatcher( filepath, 21, 28 ),
-                              LocationMatcher( filepath, 21, 45 ) ),
-            ) ,
-            'location': LocationMatcher( filepath, 15, 32 )
-          } ) )
-        }
+@SharedYcmd
+def Subcommands_RefactorRename_Simple_test( app ):
+  filepath = PathToTestFile( 'simple_test.js' )
+  RunTest( app, {
+    'description': 'RefactorRename works within a single scope/file',
+    'request': {
+      'command': 'RefactorRename',
+      'arguments': [ 'test' ],
+      'filepath': filepath,
+      'line_num': 15,
+      'column_num': 32,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': {
+        'fixits': contains( has_entries( {
+          'chunks': contains(
+              ChunkMatcher( 'test',
+                            LocationMatcher( filepath, 1, 5 ),
+                            LocationMatcher( filepath, 1, 22 ) ),
+              ChunkMatcher( 'test',
+                            LocationMatcher( filepath, 13, 25 ),
+                            LocationMatcher( filepath, 13, 42 ) ),
+              ChunkMatcher( 'test',
+                            LocationMatcher( filepath, 14, 24 ),
+                            LocationMatcher( filepath, 14, 41 ) ),
+              ChunkMatcher( 'test',
+                            LocationMatcher( filepath, 15, 24 ),
+                            LocationMatcher( filepath, 15, 41 ) ),
+              ChunkMatcher( 'test',
+                            LocationMatcher( filepath, 21, 7 ),
+                            LocationMatcher( filepath, 21, 24 ) ),
+              # On the same line, ensuring offsets are as expected (as
+              # unmodified source, similar to clang)
+              ChunkMatcher( 'test',
+                            LocationMatcher( filepath, 21, 28 ),
+                            LocationMatcher( filepath, 21, 45 ) ),
+          ),
+          'location': LocationMatcher( filepath, 15, 32 )
+        } ) )
       }
-    } )
-
+    }
+  } )
 
-  def RefactorRename_MultipleFiles_test( self ):
-    file1 = self._PathToTestFile( 'file1.js' )
-    file2 = self._PathToTestFile( 'file2.js' )
-    file3 = self._PathToTestFile( 'file3.js' )
 
-    self._RunTest( {
-      'description': 'RefactorRename works across files',
-      'request': {
-        'command': 'RefactorRename',
-        'arguments': [ 'a-quite-long-string' ],
-        'filepath': file1,
-        'line_num': 3,
-        'column_num': 14,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': {
-          'fixits': contains( has_entries( {
-            'chunks': contains(
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file1, 1, 5 ),
-                LocationMatcher( file1, 1, 11 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file1, 3, 14 ),
-                LocationMatcher( file1, 3, 19 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file2, 2, 14 ),
-                LocationMatcher( file2, 2, 19 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file3, 3, 12 ),
-                LocationMatcher( file3, 3, 17 ) )
-            ) ,
-            'location': LocationMatcher( file1, 3, 14 )
-          } ) )
-        }
+@SharedYcmd
+def Subcommands_RefactorRename_MultipleFiles_test( app ):
+  file1 = PathToTestFile( 'file1.js' )
+  file2 = PathToTestFile( 'file2.js' )
+  file3 = PathToTestFile( 'file3.js' )
+
+  RunTest( app, {
+    'description': 'RefactorRename works across files',
+    'request': {
+      'command': 'RefactorRename',
+      'arguments': [ 'a-quite-long-string' ],
+      'filepath': file1,
+      'line_num': 3,
+      'column_num': 14,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': {
+        'fixits': contains( has_entries( {
+          'chunks': contains(
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file1, 1, 5 ),
+              LocationMatcher( file1, 1, 11 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file1, 3, 14 ),
+              LocationMatcher( file1, 3, 19 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file2, 2, 14 ),
+              LocationMatcher( file2, 2, 19 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file3, 3, 12 ),
+              LocationMatcher( file3, 3, 17 ) )
+          ),
+          'location': LocationMatcher( file1, 3, 14 )
+        } ) )
       }
-    } )
+    }
+  } )
 
 
-  def RefactorRename_MultipleFiles_OnFileReadyToParse_test( self ):
-    file1 = self._PathToTestFile( 'file1.js' )
-    file2 = self._PathToTestFile( 'file2.js' )
-    file3 = self._PathToTestFile( 'file3.js' )
-
-    # This test is roughly the same as the previous one, except here file4.js is
-    # pushed into the Tern engine via 'opening it in the editor' (i.e.
-    # FileReadyToParse event). The first 3 are loaded into the tern server
-    # because they are listed in the .tern-project file's loadEagerly option.
-    file4 = self._PathToTestFile( 'file4.js' )
-
-    self._app.post_json( '/event_notification',
-                         self._BuildRequest( **{
-                           'filetype': 'javascript',
-                           'event_name': 'FileReadyToParse',
-                           'contents': ReadFile( file4 ),
-                           'filepath': file4,
-                         } ),
-                         expect_errors = False )
-
-    self._RunTest( {
-      'description': 'FileReadyToParse loads files into tern server',
-      'request': {
-        'command': 'RefactorRename',
-        'arguments': [ 'a-quite-long-string' ],
-        'filepath': file1,
-        'line_num': 3,
-        'column_num': 14,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': {
-          'fixits': contains( has_entries( {
-            'chunks': contains(
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file1, 1, 5 ),
-                LocationMatcher( file1, 1, 11 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file1, 3, 14 ),
-                LocationMatcher( file1, 3, 19 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file2, 2, 14 ),
-                LocationMatcher( file2, 2, 19 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file3, 3, 12 ),
-                LocationMatcher( file3, 3, 17 ) ),
-              ChunkMatcher(
-                'a-quite-long-string',
-                LocationMatcher( file4, 4, 22 ),
-                LocationMatcher( file4, 4, 28 ) )
-            ) ,
-            'location': LocationMatcher( file1, 3, 14 )
-          } ) )
-        }
+@SharedYcmd
+def Subcommands_RefactorRename_MultipleFiles_OnFileReadyToParse_test( app ):
+  file1 = PathToTestFile( 'file1.js' )
+  file2 = PathToTestFile( 'file2.js' )
+  file3 = PathToTestFile( 'file3.js' )
+
+  # This test is roughly the same as the previous one, except here file4.js is
+  # pushed into the Tern engine via 'opening it in the editor' (i.e.
+  # FileReadyToParse event). The first 3 are loaded into the tern server
+  # because they are listed in the .tern-project file's loadEagerly option.
+  file4 = PathToTestFile( 'file4.js' )
+
+  app.post_json( '/event_notification',
+                 BuildRequest( **{
+                   'filetype': 'javascript',
+                   'event_name': 'FileReadyToParse',
+                   'contents': ReadFile( file4 ),
+                   'filepath': file4,
+                 } ),
+                 expect_errors = False )
+
+  RunTest( app, {
+    'description': 'FileReadyToParse loads files into tern server',
+    'request': {
+      'command': 'RefactorRename',
+      'arguments': [ 'a-quite-long-string' ],
+      'filepath': file1,
+      'line_num': 3,
+      'column_num': 14,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': {
+        'fixits': contains( has_entries( {
+          'chunks': contains(
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file1, 1, 5 ),
+              LocationMatcher( file1, 1, 11 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file1, 3, 14 ),
+              LocationMatcher( file1, 3, 19 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file2, 2, 14 ),
+              LocationMatcher( file2, 2, 19 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file3, 3, 12 ),
+              LocationMatcher( file3, 3, 17 ) ),
+            ChunkMatcher(
+              'a-quite-long-string',
+              LocationMatcher( file4, 4, 22 ),
+              LocationMatcher( file4, 4, 28 ) )
+          ),
+          'location': LocationMatcher( file1, 3, 14 )
+        } ) )
       }
-    } )
+    }
+  } )
 
 
-  def RefactorRename_Missing_New_Name_test( self ):
-    self._RunTest( {
-      'description': 'FixItRename raises an error without new name',
-      'request': {
-        'command': 'FixItRename',
-        'line_num': 17,
-        'column_num': 29,
-        'filepath': self._PathToTestFile( 'coollib', 'cool_object.js' ),
+@SharedYcmd
+def Subcommands_RefactorRename_Missing_New_Name_test( app ):
+  RunTest( app, {
+    'description': 'FixItRename raises an error without new name',
+    'request': {
+      'command': 'FixItRename',
+      'line_num': 17,
+      'column_num': 29,
+      'filepath': PathToTestFile( 'coollib', 'cool_object.js' ),
+    },
+    'expect': {
+      'response': http.client.INTERNAL_SERVER_ERROR,
+      'data': {
+        'exception': ErrorMatcher(
+           ValueError,
+           'Please specify a new name to rename it to.\n'
+           'Usage: RefactorRename <new name>' ),
       },
-      'expect': {
-        'response': http.client.INTERNAL_SERVER_ERROR,
-        'data': {
-          'exception': self._ErrorMatcher(
-                                  ValueError,
-                                  'Please specify a new name to rename it to.\n'
-                                  'Usage: RefactorRename <new name>' ),
-        },
-      }
-    } )
+    }
+  } )
diff --git a/ycmd/tests/misc_handlers_test.py b/ycmd/tests/misc_handlers_test.py
--- a/ycmd/tests/misc_handlers_test.py
+++ b/ycmd/tests/misc_handlers_test.py
@@ -25,39 +25,39 @@
 from builtins import *  # noqa
 
 from nose.tools import ok_
-from .handlers_test import Handlers_test
-from ycmd.tests.test_utils import DummyCompleter
 from hamcrest import assert_that, contains
 
+from ycmd.tests import SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, DummyCompleter, PatchCompleter
 
-class MiscHandlers_test( Handlers_test ):
 
-  def SemanticCompletionAvailable_test( self ):
-    with self.PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
-      request_data = self._BuildRequest( filetype = 'dummy_filetype' )
-      ok_( self._app.post_json( '/semantic_completion_available',
-                                request_data ).json )
+@SharedYcmd
+def MiscHandlers_SemanticCompletionAvailable_test( app ):
+  with PatchCompleter( DummyCompleter, filetype = 'dummy_filetype' ):
+    request_data = BuildRequest( filetype = 'dummy_filetype' )
+    ok_( app.post_json( '/semantic_completion_available', request_data ).json )
 
 
-  def EventNotification_AlwaysJsonResponse_test( self ):
-    event_data = self._BuildRequest( contents = 'foo foogoo ba',
-                                     event_name = 'FileReadyToParse' )
+@SharedYcmd
+def MiscHandlers_EventNotification_AlwaysJsonResponse_test( app ):
+  event_data = BuildRequest( contents = 'foo foogoo ba',
+                             event_name = 'FileReadyToParse' )
 
-    self._app.post_json( '/event_notification', event_data ).json
+  app.post_json( '/event_notification', event_data ).json
 
 
-  def FilterAndSortCandidates_Basic_test( self ):
-    candidate1 = { 'prop1': 'aoo', 'prop2': 'bar' }
-    candidate2 = { 'prop1': 'bfo', 'prop2': 'zoo' }
-    candidate3 = { 'prop1': 'cfo', 'prop2': 'moo' }
+@SharedYcmd
+def MiscHandlers_FilterAndSortCandidates_Basic_test( app ):
+  candidate1 = { 'prop1': 'aoo', 'prop2': 'bar' }
+  candidate2 = { 'prop1': 'bfo', 'prop2': 'zoo' }
+  candidate3 = { 'prop1': 'cfo', 'prop2': 'moo' }
 
-    data = {
-      'candidates': [ candidate3, candidate1, candidate2 ],
-      'sort_property': 'prop1',
-      'query': 'fo'
-    }
+  data = {
+    'candidates': [ candidate3, candidate1, candidate2 ],
+    'sort_property': 'prop1',
+    'query': 'fo'
+  }
 
-    response_data = self._app.post_json(
-      '/filter_and_sort_candidates', data ).json
+  response_data = app.post_json( '/filter_and_sort_candidates', data ).json
 
-    assert_that( response_data, contains( candidate2, candidate3 ) )
+  assert_that( response_data, contains( candidate2, candidate3 ) )
diff --git a/ycmd/tests/python/__init__.py b/ycmd/tests/python/__init__.py
--- a/ycmd/tests/python/__init__.py
+++ b/ycmd/tests/python/__init__.py
@@ -0,0 +1,113 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+import time
+
+from ycmd import handlers
+from ycmd.tests.test_utils import BuildRequest, SetUpApp
+
+shared_app = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def WaitUntilJediHTTPServerReady( app ):
+  retries = 100
+
+  while retries > 0:
+    result = app.get( '/ready', { 'subserver': 'python' } ).json
+    if result:
+      return
+
+    time.sleep( 0.2 )
+    retries = retries - 1
+
+  raise RuntimeError( "Timeout waiting for JediHTTP" )
+
+
+def StopJediHTTPServer( app ):
+  # We don't actually start a JediHTTP server on every test, so we just
+  # ignore errors when stopping the server
+  app.post_json( '/run_completer_command',
+                 BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'StopServer' ],
+                               filetype = 'python' ),
+                 expect_errors = True )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+  WaitUntilJediHTTPServerReady( shared_app )
+
+
+def tearDownPackage():
+  """Cleans up the tests using the SharedYcmd decorator in this package. It is
+  executed once after running all the tests in the package."""
+  global shared_app
+
+  StopJediHTTPServer( shared_app )
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
+
+
+def IsolatedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes a unique ycmd application as a parameter. It should be used on tests
+  that change the server state in a irreversible way (ex: a semantic subserver
+  is stopped or restarted) or expect a clean state (ex: no semantic subserver
+  started, no .ycm_extra_conf.py loaded, etc).
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    old_server_state = handlers._server_state
+
+    try:
+      test( SetUpApp(), *args, **kwargs )
+    finally:
+      handlers._server_state = old_server_state
+  return Wrapper
diff --git a/ycmd/tests/python/get_completions_test.py b/ycmd/tests/python/get_completions_test.py
--- a/ycmd/tests/python/get_completions_test.py
+++ b/ycmd/tests/python/get_completions_test.py
@@ -28,114 +28,112 @@
 from nose.tools import eq_
 from hamcrest import ( assert_that, has_item, has_items, has_entry,
                        has_entries, contains, empty, contains_string )
-from .python_handlers_test import Python_Handlers_test
 from ycmd.utils import ReadFile
+from ycmd.tests.python import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import ( BuildRequest, CompletionEntryMatcher,
+                                    CompletionLocationMatcher )
 import http.client
 
 
-class Python_GetCompletions_test( Python_Handlers_test ):
-
-  def setUp( self ):
-    super( Python_GetCompletions_test, self ).setUp()
-    self.WaitUntilJediHTTPServerReady()
-
-
-  def _RunTest( self, test ):
-    """
-    Method to run a simple completion test and verify the result
-
-    test is a dictionary containing:
-      'request': kwargs for BuildRequest
-      'expect': {
-         'response': server response code (e.g. httplib.OK)
-         'data': matcher for the server response json
-      }
-    """
-    contents = ReadFile( test[ 'request' ][ 'filepath' ] )
-
-    def CombineRequest( request, data ):
-      kw = request
-      request.update( data )
-      return self._BuildRequest( **kw )
-
-    self._app.post_json( '/event_notification',
-                         CombineRequest( test[ 'request' ], {
-                                         'event_name': 'FileReadyToParse',
-                                         'contents': contents,
-                                         } ) )
-
-    # We ignore errors here and we check the response code ourself.
-    # This is to allow testing of requests returning errors.
-    response = self._app.post_json( '/completions',
-                                    CombineRequest( test[ 'request' ], {
-                                      'contents': contents
-                                    } ),
-                                    expect_errors = True )
-
-    eq_( response.status_code, test[ 'expect' ][ 'response' ] )
-
-    assert_that( response.json, test[ 'expect' ][ 'data' ] )
-
-
-  def Basic_test( self ):
-    filepath = self._PathToTestFile( 'basic.py' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'python',
-                                          contents = ReadFile( filepath ),
-                                          line_num = 7,
-                                          column_num = 3)
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-
-    assert_that( results,
-                 has_items(
-                   self._CompletionEntryMatcher( 'a' ),
-                   self._CompletionEntryMatcher( 'b' ),
-                   self._CompletionLocationMatcher( 'line_num', 3 ),
-                   self._CompletionLocationMatcher( 'line_num', 4 ),
-                   self._CompletionLocationMatcher( 'column_num', 10 ),
-                   self._CompletionLocationMatcher( 'filepath', filepath ) ) )
-
-
-  def UnicodeDescription_test( self ):
-    filepath = self._PathToTestFile( 'unicode.py' )
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'python',
-                                          contents = ReadFile( filepath ),
-                                          force_semantic = True,
-                                          line_num = 5,
-                                          column_num = 3)
-
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
-    assert_that( results, has_item(
-      has_entry( 'detailed_info', contains_string( u'aafäö' ) ) ) )
-
-
-  def NoSuggestions_Fallback_test( self ):
-    # Python completer doesn't raise NO_COMPLETIONS_MESSAGE, so this is a
-    # different code path to the Clang completer cases
-
-    # TESTCASE2 (general_fallback/lang_python.py)
-    self._RunTest( {
-      'description': 'param jedi does not know about (id). query="a_p"',
-      'request': {
-        'filetype'  : 'python',
-        'filepath'  : self._PathToTestFile( 'general_fallback',
-                                            'lang_python.py' ),
-        'line_num'  : 28,
-        'column_num': 20,
-        'force_semantic': False,
-      },
-      'expect': {
-        'response': http.client.OK,
-        'data': has_entries( {
-          'completions': contains(
-            self._CompletionEntryMatcher( 'a_parameter', '[ID]' ),
-            self._CompletionEntryMatcher( 'another_parameter', '[ID]' ),
-          ),
-          'errors': empty(),
-        } )
-      },
-    } )
+@SharedYcmd
+def GetCompletions_Basic_test( app ):
+  filepath = PathToTestFile( 'basic.py' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'python',
+                                  contents = ReadFile( filepath ),
+                                  line_num = 7,
+                                  column_num = 3)
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+
+  assert_that( results,
+               has_items(
+                 CompletionEntryMatcher( 'a' ),
+                 CompletionEntryMatcher( 'b' ),
+                 CompletionLocationMatcher( 'line_num', 3 ),
+                 CompletionLocationMatcher( 'line_num', 4 ),
+                 CompletionLocationMatcher( 'column_num', 10 ),
+                 CompletionLocationMatcher( 'filepath', filepath ) ) )
+
+
+@SharedYcmd
+def GetCompletions_UnicodeDescription_test( app ):
+  filepath = PathToTestFile( 'unicode.py' )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'python',
+                                  contents = ReadFile( filepath ),
+                                  force_semantic = True,
+                                  line_num = 5,
+                                  column_num = 3)
+
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
+  assert_that( results, has_item(
+    has_entry( 'detailed_info', contains_string( u'aafäö' ) ) ) )
+
+
+def RunTest( app, test ):
+  """
+  Method to run a simple completion test and verify the result
+
+  test is a dictionary containing:
+    'request': kwargs for BuildRequest
+    'expect': {
+       'response': server response code (e.g. httplib.OK)
+       'data': matcher for the server response json
+    }
+  """
+  contents = ReadFile( test[ 'request' ][ 'filepath' ] )
+
+  def CombineRequest( request, data ):
+    kw = request
+    request.update( data )
+    return BuildRequest( **kw )
+
+  app.post_json( '/event_notification',
+                 CombineRequest( test[ 'request' ], {
+                                 'event_name': 'FileReadyToParse',
+                                 'contents': contents,
+                                 } ) )
+
+  # We ignore errors here and we check the response code ourself.
+  # This is to allow testing of requests returning errors.
+  response = app.post_json( '/completions',
+                            CombineRequest( test[ 'request' ], {
+                              'contents': contents
+                            } ),
+                            expect_errors = True )
+
+  eq_( response.status_code, test[ 'expect' ][ 'response' ] )
+
+  assert_that( response.json, test[ 'expect' ][ 'data' ] )
+
+
+@SharedYcmd
+def GetCompletions_NoSuggestions_Fallback_test( app ):
+  # Python completer doesn't raise NO_COMPLETIONS_MESSAGE, so this is a
+  # different code path to the Clang completer cases
+
+  # TESTCASE2 (general_fallback/lang_python.py)
+  RunTest( app, {
+    'description': 'param jedi does not know about (id). query="a_p"',
+    'request': {
+      'filetype'  : 'python',
+      'filepath'  : PathToTestFile( 'general_fallback',
+                                    'lang_python.py' ),
+      'line_num'  : 28,
+      'column_num': 20,
+      'force_semantic': False,
+    },
+    'expect': {
+      'response': http.client.OK,
+      'data': has_entries( {
+        'completions': contains(
+          CompletionEntryMatcher( 'a_parameter', '[ID]' ),
+          CompletionEntryMatcher( 'another_parameter', '[ID]' ),
+        ),
+        'errors': empty(),
+      } )
+    },
+  } )
diff --git a/ycmd/tests/python/python_handlers_test.py b/ycmd/tests/python/python_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/python/python_handlers_test.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-import time
-from ..handlers_test import Handlers_test
-
-
-class Python_Handlers_test( Handlers_test ):
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def tearDown( self ):
-    self.StopJediHTTPServer()
-
-
-  def WaitUntilJediHTTPServerReady( self ):
-    retries = 100
-
-    while retries > 0:
-      result = self._app.get( '/ready', { 'subserver': 'python' } ).json
-      if result:
-        return
-
-      time.sleep( 0.2 )
-      retries = retries - 1
-
-    raise RuntimeError( "Timeout waiting for JediHTTP" )
-
-
-  def StopJediHTTPServer( self ):
-    request = self._BuildRequest( completer_target = 'filetype_default',
-                                  command_arguments = [ 'StopServer' ],
-                                  filetype = 'python' )
-    # We don't actually start a JediHTTP server on every test, so we just
-    # ignore errors when stopping the server
-    self._app.post_json( '/run_completer_command',
-                         request,
-                         expect_errors = True )
diff --git a/ycmd/tests/python/subcommands_test.py b/ycmd/tests/python/subcommands_test.py
--- a/ycmd/tests/python/subcommands_test.py
+++ b/ycmd/tests/python/subcommands_test.py
@@ -25,45 +25,57 @@
 
 from hamcrest import assert_that
 from nose.tools import eq_
-from .python_handlers_test import Python_Handlers_test
-from ycmd.utils import ReadFile
 import os.path
 
-
-class Python_Subcommands_test( Python_Handlers_test ):
-
-  def setUp( self ):
-    super( Python_Subcommands_test, self ).setUp()
-    self.WaitUntilJediHTTPServerReady()
-
-
-  def GoTo_Variation_ZeroBasedLineAndColumn_test( self ):
-    tests = [
-          {
-            'command_arguments': [ 'GoToDefinition' ],
-            'response': {
-              'filepath': os.path.abspath( '/foo.py' ),
-              'line_num': 2,
-              'column_num': 5
-            }
-          },
-          {
-            'command_arguments': [ 'GoToDeclaration' ],
-            'response': {
-              'filepath': os.path.abspath( '/foo.py' ),
-              'line_num': 7,
-              'column_num': 1
-            }
-          }
-      ]
-    for test in tests:
-      yield self._Run_GoTo_Variation_ZeroBasedLineAndColumn, test
-
-
-  def _Run_GoTo_Variation_ZeroBasedLineAndColumn( self, test ):
-    # Example taken directly from jedi docs
-    # http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html#examples
-    contents = """
+from ycmd.utils import ReadFile
+from ycmd.tests.python import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, ErrorMatcher
+
+
+@SharedYcmd
+def RunGoToTest( app, test ):
+  filepath = PathToTestFile( test[ 'request' ][ 'filename' ] )
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ 'GoTo' ],
+                            line_num = test[ 'request' ][ 'line_num' ],
+                            contents = ReadFile( filepath ),
+                            filetype = 'python',
+                            filepath = filepath )
+
+  eq_( test[ 'response' ],
+       app.post_json( '/run_completer_command', goto_data ).json )
+
+
+def Subcommands_GoTo_test():
+  # Tests taken from https://github.com/Valloric/YouCompleteMe/issues/1236
+  tests = [
+    {
+      'request': { 'filename': 'goto_file1.py', 'line_num': 2 },
+      'response': {
+          'filepath': PathToTestFile( 'goto_file3.py' ),
+          'line_num': 1,
+          'column_num': 5
+      }
+    },
+    {
+      'request': { 'filename': 'goto_file4.py', 'line_num': 2 },
+      'response': {
+          'filepath': PathToTestFile( 'goto_file4.py' ),
+          'line_num': 1,
+          'column_num': 18
+      }
+    }
+  ]
+
+  for test in tests:
+    yield RunGoToTest, test
+
+
+@SharedYcmd
+def RunGoToTest_Variation_ZeroBasedLineAndColumn( app, test ):
+  # Example taken directly from jedi docs
+  # http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html#examples
+  contents = """
 def my_func():
   print 'called'
 
@@ -74,145 +86,140 @@ def my_func():
 inception()
 """
 
-    goto_data = self._BuildRequest(
-        completer_target = 'filetype_default',
-        command_arguments = test[ 'command_arguments' ],
-        line_num = 9,
-        contents = contents,
-        filetype = 'python',
-        filepath = '/foo.py'
-    )
-
-    eq_( test[ 'response' ],
-         self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GoToDefinition_NotFound_test( self ):
-    filepath = self._PathToTestFile( 'goto_file5.py' )
-    goto_data = self._BuildRequest( command_arguments = [ 'GoToDefinition' ],
-                                    line_num = 4,
-                                    contents = ReadFile( filepath ),
-                                    filetype = 'python',
-                                    filepath = filepath )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    goto_data,
-                                    expect_errors = True  ).json
-    assert_that( response,
-                 self._ErrorMatcher( RuntimeError,
-                                     "Can\'t jump to definition." ) )
-
-
-  def GoTo_test( self ):
-    # Tests taken from https://github.com/Valloric/YouCompleteMe/issues/1236
-    tests = [
-        {
-          'request': { 'filename': 'goto_file1.py', 'line_num': 2 },
-          'response': {
-              'filepath': self._PathToTestFile( 'goto_file3.py' ),
-              'line_num': 1,
-              'column_num': 5
-          }
-        },
-        {
-          'request': { 'filename': 'goto_file4.py', 'line_num': 2 },
-          'response': {
-              'filepath': self._PathToTestFile( 'goto_file4.py' ),
-              'line_num': 1,
-              'column_num': 18
-          }
-        }
-    ]
-    for test in tests:
-      yield self._Run_GoTo, test
-
-
-  def _Run_GoTo( self, test ):
-    filepath = self._PathToTestFile( test[ 'request' ][ 'filename' ] )
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ 'GoTo' ],
-                                    line_num = test[ 'request' ][ 'line_num' ],
-                                    contents = ReadFile( filepath ),
-                                    filetype = 'python',
-                                    filepath = filepath )
-
-    eq_( test[ 'response' ],
-         self._app.post_json( '/run_completer_command', goto_data ).json )
-
-
-  def GetDoc_Method_test( self ):
-    # Testcase1
-    filepath = self._PathToTestFile( 'GetDoc.py' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'python',
-                                     line_num = 17,
-                                     column_num = 9,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command', event_data ).json
-
-    eq_( response, {
-      'detailed_info': '_ModuleMethod()\n\n'
-                       'Module method docs\n'
-                       'Are dedented, like you might expect',
-    } )
-
-
-  def GetDoc_Class_test( self ):
-    # Testcase1
-    filepath = self._PathToTestFile( 'GetDoc.py' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'python',
-                                     line_num = 19,
-                                     column_num = 2,
-                                     contents = contents,
-                                     command_arguments = [ 'GetDoc' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command', event_data ).json
-
-    eq_( response, {
-      'detailed_info': 'Class Documentation',
-    } )
-
-
-  def GoToReferences_test( self ):
-    filepath = self._PathToTestFile( 'goto_references.py' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'python',
-                                     line_num = 4,
-                                     column_num = 5,
-                                     contents = contents,
-                                     command_arguments = [ 'GoToReferences' ],
-                                     completer_target = 'filetype_default' )
-
-    response = self._app.post_json( '/run_completer_command', event_data ).json
-
-    eq_( response, [ {
-      'filepath': self._PathToTestFile( 'goto_references.py' ),
+  goto_data = BuildRequest(
+      completer_target = 'filetype_default',
+      command_arguments = test[ 'command_arguments' ],
+      line_num = 9,
+      contents = contents,
+      filetype = 'python',
+      filepath = '/foo.py'
+  )
+
+  eq_( test[ 'response' ],
+       app.post_json( '/run_completer_command', goto_data ).json )
+
+
+def Subcommands_GoTo_Variation_ZeroBasedLineAndColumn_test():
+  tests = [
+    {
+      'command_arguments': [ 'GoToDefinition' ],
+      'response': {
+        'filepath': os.path.abspath( '/foo.py' ),
+        'line_num': 2,
+        'column_num': 5
+      }
+    },
+    {
+      'command_arguments': [ 'GoToDeclaration' ],
+      'response': {
+        'filepath': os.path.abspath( '/foo.py' ),
+        'line_num': 7,
+        'column_num': 1
+      }
+    }
+  ]
+
+  for test in tests:
+    yield RunGoToTest_Variation_ZeroBasedLineAndColumn, test
+
+
+@SharedYcmd
+def Subcommands_GoToDefinition_NotFound_test( app ):
+  filepath = PathToTestFile( 'goto_file5.py' )
+  goto_data = BuildRequest( command_arguments = [ 'GoToDefinition' ],
+                            line_num = 4,
+                            contents = ReadFile( filepath ),
+                            filetype = 'python',
+                            filepath = filepath )
+
+  response = app.post_json( '/run_completer_command',
+                            goto_data,
+                            expect_errors = True  ).json
+  assert_that( response,
+               ErrorMatcher( RuntimeError,
+                             "Can\'t jump to definition." ) )
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Method_test( app ):
+  # Testcase1
+  filepath = PathToTestFile( 'GetDoc.py' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'python',
+                             line_num = 17,
+                             column_num = 9,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command', event_data ).json
+
+  eq_( response, {
+    'detailed_info': '_ModuleMethod()\n\n'
+                     'Module method docs\n'
+                     'Are dedented, like you might expect',
+  } )
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Class_test( app ):
+  # Testcase1
+  filepath = PathToTestFile( 'GetDoc.py' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'python',
+                             line_num = 19,
+                             column_num = 2,
+                             contents = contents,
+                             command_arguments = [ 'GetDoc' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command', event_data ).json
+
+  eq_( response, {
+    'detailed_info': 'Class Documentation',
+  } )
+
+
+@SharedYcmd
+def Subcommands_GoToReferences_test( app ):
+  filepath = PathToTestFile( 'goto_references.py' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'python',
+                             line_num = 4,
+                             column_num = 5,
+                             contents = contents,
+                             command_arguments = [ 'GoToReferences' ],
+                             completer_target = 'filetype_default' )
+
+  response = app.post_json( '/run_completer_command', event_data ).json
+
+  eq_( response, [
+    {
+      'filepath': PathToTestFile( 'goto_references.py' ),
       'column_num': 5,
       'description': 'def f',
       'line_num': 1
-    }, {
-      'filepath': self._PathToTestFile( 'goto_references.py' ),
+    },
+    {
+      'filepath': PathToTestFile( 'goto_references.py' ),
       'column_num': 5,
       'description': 'a = f()',
       'line_num': 4
-    }, {
-      'filepath': self._PathToTestFile( 'goto_references.py' ),
+    },
+    {
+      'filepath': PathToTestFile( 'goto_references.py' ),
       'column_num': 5,
       'description': 'b = f()',
       'line_num': 5
-    }, {
-      'filepath': self._PathToTestFile( 'goto_references.py' ),
+    },
+    {
+      'filepath': PathToTestFile( 'goto_references.py' ),
       'column_num': 5,
       'description': 'c = f()',
       'line_num': 6
diff --git a/ycmd/tests/python/user_defined_python_test.py b/ycmd/tests/python/user_defined_python_test.py
--- a/ycmd/tests/python/user_defined_python_test.py
+++ b/ycmd/tests/python/user_defined_python_test.py
@@ -23,13 +23,15 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from .python_handlers_test import Python_Handlers_test
+from hamcrest.core.base_matcher import BaseMatcher
+from hamcrest import assert_that, has_item, contains, equal_to, is_not  # noqa
 from mock import patch
+import sys
+
 from ycmd import utils
 from ycmd.completers.python.jedi_completer import BINARY_NOT_FOUND_MESSAGE
-from hamcrest.core.base_matcher import BaseMatcher
-from hamcrest import assert_that, has_item, contains, equal_to, is_not # noqa
-import sys
+from ycmd.tests.python import IsolatedYcmd
+from ycmd.tests.test_utils import BuildRequest, ErrorMatcher, UserOption
 
 
 class CalledWith( BaseMatcher ):
@@ -75,79 +77,79 @@ def was_called_with_python( python ):
   return CalledWith( python )
 
 
-
-class UserDefinedPython_test( Python_Handlers_test ):
-
-  @patch( 'ycmd.utils.SafePopen' )
-  def WithoutAnyOption_DefaultToYcmdPython_test( self, *args ):
-    self._app.get( '/ready', { 'subserver': 'python' } )
-    assert_that( utils.SafePopen, was_called_with_python( sys.executable ) )
-
-
-  @patch( 'ycmd.utils.SafePopen' )
-  @patch( 'ycmd.completers.python.jedi_completer.'
-            'JediCompleter._CheckBinaryExists',
-          return_value = False )
-  def WhenNonExistentPythonIsGiven_ReturnAnError_test( self, *args ):
-    python = '/non/existing/path/python'
-    with self.UserOption( 'python_binary_path', python ):
-      response = self._app.get( '/ready',
-                                { 'subserver': 'python' },
-                                expect_errors = True ).json
-
-      msg = BINARY_NOT_FOUND_MESSAGE.format( python )
-      assert_that( response, self._ErrorMatcher( RuntimeError, msg ) )
-      utils.SafePopen.assert_not_called()
-
-
-  @patch( 'ycmd.utils.SafePopen' )
-  @patch( 'ycmd.completers.python.jedi_completer.'
-            'JediCompleter._CheckBinaryExists',
-          return_value = True )
-  def WhenExistingPythonIsGiven_ThatIsUsed_test( self, *args ):
-    python = '/existing/python'
-    with self.UserOption( 'python_binary_path', python ):
-      self._app.get( '/ready', { 'subserver': 'python' } ).json
-      assert_that( utils.SafePopen, was_called_with_python( python ) )
-
-
-  @patch( 'ycmd.utils.SafePopen' )
-  @patch( 'ycmd.completers.python.jedi_completer.'
-            'JediCompleter._CheckBinaryExists',
-          return_value = True )
-  def RestartServerWithoutArguments_WillReuseTheLastPython_test( self, *args ):
-    request = self._BuildRequest( filetype = 'python',
-                                  command_arguments = [ 'RestartServer' ] )
-    self._app.post_json( '/run_completer_command', request )
-    assert_that( utils.SafePopen, was_called_with_python( sys.executable ) )
-
-
-  @patch( 'ycmd.utils.SafePopen' )
-  @patch( 'ycmd.completers.python.jedi_completer.'
-            'JediCompleter._CheckBinaryExists',
-          return_value = True )
-  def RestartServerWithArgument_WillUseTheSpecifiedPython_test( self, *args ):
-    python = '/existing/python'
-    request = self._BuildRequest( filetype = 'python',
-                                  command_arguments = [ 'RestartServer',
-                                                        python ] )
-    self._app.post_json( '/run_completer_command', request )
-    assert_that( utils.SafePopen, was_called_with_python( python ) )
+@IsolatedYcmd
+@patch( 'ycmd.utils.SafePopen' )
+def UserDefinedPython_WithoutAnyOption_DefaultToYcmdPython_test( app, *args ):
+  app.get( '/ready', { 'subserver': 'python' } )
+  assert_that( utils.SafePopen, was_called_with_python( sys.executable ) )
 
 
-  @patch( 'ycmd.utils.SafePopen' )
-  @patch( 'ycmd.completers.python.jedi_completer.'
-            'JediCompleter._CheckBinaryExists',
-          return_value = False )
-  def RestartServerWithNonExistingPythonArgument_test( self, *args ):
-    python = '/non/existing/python'
-    request = self._BuildRequest( filetype = 'python',
-                                  command_arguments = [ 'RestartServer',
-                                                        python ] )
-    response = self._app.post_json( '/run_completer_command',
-                                    request,
-                                    expect_errors = True ).json
+@IsolatedYcmd
+@patch( 'ycmd.utils.SafePopen' )
+@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.'
+          '_CheckBinaryExists', return_value = False )
+def UserDefinedPython_WhenNonExistentPythonIsGiven_ReturnAnError_test( app,
+                                                                       *args ):
+  python = '/non/existing/path/python'
+  with UserOption( 'python_binary_path', python ):
+    response = app.get( '/ready',
+                        { 'subserver': 'python' },
+                        expect_errors = True ).json
 
     msg = BINARY_NOT_FOUND_MESSAGE.format( python )
-    assert_that( response, self._ErrorMatcher( RuntimeError, msg ) )
-    assert_that( utils.SafePopen, was_called_with_python( sys.executable ) )
+    assert_that( response, ErrorMatcher( RuntimeError, msg ) )
+    utils.SafePopen.assert_not_called()
+
+
+@IsolatedYcmd
+@patch( 'ycmd.utils.SafePopen' )
+@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.'
+          '_CheckBinaryExists', return_value = True )
+def UserDefinedPython_WhenExistingPythonIsGiven_ThatIsUsed_test( app, *args ):
+  python = '/existing/python'
+  with UserOption( 'python_binary_path', python ):
+    app.get( '/ready', { 'subserver': 'python' } ).json
+    assert_that( utils.SafePopen, was_called_with_python( python ) )
+
+
+@IsolatedYcmd
+@patch( 'ycmd.utils.SafePopen' )
+@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.'
+          '_CheckBinaryExists', return_value = True )
+def UserDefinedPython_RestartServerWithoutArguments_WillReuseTheLastPython_test(
+  app, *args ):
+  request = BuildRequest( filetype = 'python',
+                          command_arguments = [ 'RestartServer' ] )
+  app.post_json( '/run_completer_command', request )
+  assert_that( utils.SafePopen, was_called_with_python( sys.executable ) )
+
+
+@IsolatedYcmd
+@patch( 'ycmd.utils.SafePopen' )
+@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.'
+          '_CheckBinaryExists', return_value = True )
+def UserDefinedPython_RestartServerWithArgument_WillUseTheSpecifiedPython_test(
+  app, *args ):
+  python = '/existing/python'
+  request = BuildRequest( filetype = 'python',
+                          command_arguments = [ 'RestartServer', python ] )
+  app.post_json( '/run_completer_command', request )
+  assert_that( utils.SafePopen, was_called_with_python( python ) )
+
+
+@IsolatedYcmd
+@patch( 'ycmd.utils.SafePopen' )
+@patch( 'ycmd.completers.python.jedi_completer.JediCompleter.'
+          '_CheckBinaryExists', return_value = False )
+def UserDefinedPython_RestartServerWithNonExistingPythonArgument_test( app,
+                                                                       *args ):
+  python = '/non/existing/python'
+  request = BuildRequest( filetype = 'python',
+                          command_arguments = [ 'RestartServer', python ] )
+  response = app.post_json( '/run_completer_command',
+                            request,
+                            expect_errors = True ).json
+
+  msg = BINARY_NOT_FOUND_MESSAGE.format( python )
+  assert_that( response, ErrorMatcher( RuntimeError, msg ) )
+  assert_that( utils.SafePopen, was_called_with_python( sys.executable ) )
diff --git a/ycmd/tests/rust/__init__.py b/ycmd/tests/rust/__init__.py
--- a/ycmd/tests/rust/__init__.py
+++ b/ycmd/tests/rust/__init__.py
@@ -0,0 +1,115 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+import time
+
+from ycmd.tests.test_utils import BuildRequest, SetUpApp
+from ycmd import handlers
+
+shared_app = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def WaitUntilRacerdServerReady( app ):
+  retries = 100
+
+  while retries > 0:
+    result = app.get( '/ready', { 'subserver': 'rust' } ).json
+    if result:
+      return
+
+    time.sleep( 0.2 )
+    retries = retries - 1
+
+  raise RuntimeError( "Timeout waiting for JediHTTP" )
+
+
+def StopRacerdServer( app ):
+  app.post_json( '/run_completer_command',
+                 BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'StopServer' ],
+                               filetype = 'rust' ),
+                 expect_errors = True )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+
+  WaitUntilRacerdServerReady( shared_app )
+
+
+def tearDownPackage():
+  """Cleans up the tests using the SharedYcmd decorator in this package. It is
+  executed once after running all the tests in the package."""
+  global shared_app
+
+  StopRacerdServer( shared_app )
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
+
+
+def IsolatedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes a unique ycmd application as a parameter. It should be used on tests
+  that change the server state in a irreversible way (ex: a semantic subserver
+  is stopped or restarted) or expect a clean state (ex: no semantic subserver
+  started, no .ycm_extra_conf.py loaded, etc).
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    old_server_state = handlers._server_state
+
+    try:
+      app = SetUpApp()
+      WaitUntilRacerdServerReady( app )
+      test( app, *args, **kwargs )
+      StopRacerdServer( app )
+    finally:
+      handlers._server_state = old_server_state
+  return Wrapper
diff --git a/ycmd/tests/rust/get_completions_test.py b/ycmd/tests/rust/get_completions_test.py
--- a/ycmd/tests/rust/get_completions_test.py
+++ b/ycmd/tests/rust/get_completions_test.py
@@ -23,51 +23,51 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from ycmd.utils import ReadFile
 from hamcrest import assert_that, has_entry, has_items, contains_string
-from .rust_handlers_test import Rust_Handlers_test
-
-
-class Rust_GetCompletions_test( Rust_Handlers_test ):
 
+from ycmd.tests.rust import IsolatedYcmd, PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher
+from ycmd.utils import ReadFile
 
-  def Basic_test( self ):
-    filepath = self._PathToTestFile( 'test.rs' )
-    contents = ReadFile( filepath )
-
-    self._WaitUntilServerReady()
 
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'rust',
-                                          contents = contents,
-                                          force_semantic = True,
-                                          line_num = 9,
-                                          column_num = 11 )
+@SharedYcmd
+def GetCompletions_Basic_test( app ):
+  filepath = PathToTestFile( 'test.rs' )
+  contents = ReadFile( filepath )
 
-    results = self._app.post_json( '/completions',
-                                   completion_data ).json[ 'completions' ]
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'rust',
+                                  contents = contents,
+                                  force_semantic = True,
+                                  line_num = 9,
+                                  column_num = 11 )
 
-    assert_that( results,
-                 has_items( self._CompletionEntryMatcher( 'build_rocket' ),
-                            self._CompletionEntryMatcher( 'build_shuttle' ) ) )
+  results = app.post_json( '/completions',
+                           completion_data ).json[ 'completions' ]
 
+  assert_that( results,
+               has_items( CompletionEntryMatcher( 'build_rocket' ),
+                          CompletionEntryMatcher( 'build_shuttle' ) ) )
 
-  def WhenStandardLibraryCompletionFails_MentionRustSrcPath_test( self ):
-    filepath = self._PathToTestFile( 'std_completions.rs' )
-    contents = ReadFile( filepath )
 
-    self._WaitUntilServerReady()
+# This test is isolated because it affects the GoTo tests, although it
+# shouldn't.
+@IsolatedYcmd
+def GetCompletions_WhenStandardLibraryCompletionFails_MentionRustSrcPath_test(
+  app ):
+  filepath = PathToTestFile( 'std_completions.rs' )
+  contents = ReadFile( filepath )
 
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'rust',
-                                          contents = contents,
-                                          force_semantic = True,
-                                          line_num = 5,
-                                          column_num = 11 )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'rust',
+                                  contents = contents,
+                                  force_semantic = True,
+                                  line_num = 5,
+                                  column_num = 11 )
 
-    response = self._app.post_json( '/completions',
-                                    completion_data,
-                                    expect_errors = True ).json
-    assert_that( response,
-                 has_entry( 'message',
-                            contains_string( 'rust_src_path' ) ) )
+  response = app.post_json( '/completions',
+                            completion_data,
+                            expect_errors = True ).json
+  assert_that( response,
+               has_entry( 'message',
+                          contains_string( 'rust_src_path' ) ) )
diff --git a/ycmd/tests/rust/rust_handlers_test.py b/ycmd/tests/rust/rust_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/rust/rust_handlers_test.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from ..handlers_test import Handlers_test
-
-import time
-
-
-class Rust_Handlers_test( Handlers_test ):
-
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def tearDown( self ):
-    self._StopServer()
-
-
-  def _StopServer( self ):
-    try:
-      self._app.post_json(
-        '/run_completer_command',
-        self._BuildRequest( command_arguments = [ 'StopServer' ],
-                            filetype = 'rust',
-                            completer_target = 'filetype_default' )
-      )
-    except:
-      pass
-
-
-  def _WaitUntilServerReady( self ):
-    retries = 100
-
-    while retries > 0:
-      result = self._app.get( '/ready', { 'subserver': 'rust' } ).json
-      if result:
-        return
-      time.sleep( 0.2 )
-      retries = retries - 1
-
-    raise RuntimeError( "Timeout waiting for racerd" )
diff --git a/ycmd/tests/rust/subcommands_test.py b/ycmd/tests/rust/subcommands_test.py
--- a/ycmd/tests/rust/subcommands_test.py
+++ b/ycmd/tests/rust/subcommands_test.py
@@ -23,43 +23,41 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from .rust_handlers_test import Rust_Handlers_test
 from nose.tools import eq_
-from ycmd.utils import ReadFile
-
-
-class Rust_Subcommands_test( Rust_Handlers_test ):
 
+from ycmd.tests.rust import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest
+from ycmd.utils import ReadFile
 
-  def _GoTo( self, params ):
-    filepath = self._PathToTestFile( 'test.rs' )
-    contents = ReadFile( filepath )
 
-    self._WaitUntilServerReady()
+@SharedYcmd
+def RunGoToTest( app, params ):
+  filepath = PathToTestFile( 'test.rs' )
+  contents = ReadFile( filepath )
 
-    command = params[ 'command' ]
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ command ],
-                                    line_num = 7,
-                                    column_num = 12,
-                                    contents = contents,
-                                    filetype = 'rust',
-                                    filepath = filepath )
+  command = params[ 'command' ]
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ command ],
+                            line_num = 7,
+                            column_num = 12,
+                            contents = contents,
+                            filetype = 'rust',
+                            filepath = filepath )
 
-    results = self._app.post_json( '/run_completer_command',
-                                   goto_data )
+  results = app.post_json( '/run_completer_command',
+                           goto_data )
 
-    eq_( {
-      'line_num': 1, 'column_num': 8, 'filepath': filepath
-    }, results.json )
+  eq_( {
+    'line_num': 1, 'column_num': 8, 'filepath': filepath
+  }, results.json )
 
 
-  def GoTo_all_test( self ):
-    tests = [
-      { 'command': 'GoTo' },
-      { 'command': 'GoToDefinition' },
-      { 'command': 'GoToDeclaration' }
-    ]
+def Subcommands_GoTo_all_test():
+  tests = [
+    { 'command': 'GoTo' },
+    { 'command': 'GoToDefinition' },
+    { 'command': 'GoToDeclaration' }
+  ]
 
-    for test in tests:
-      yield ( self._GoTo, test )
+  for test in tests:
+    yield RunGoToTest, test
diff --git a/ycmd/tests/subcommands_test.py b/ycmd/tests/subcommands_test.py
--- a/ycmd/tests/subcommands_test.py
+++ b/ycmd/tests/subcommands_test.py
@@ -24,36 +24,34 @@
 standard_library.install_aliases()
 from builtins import *  # noqa
 
-from nose.tools import eq_
-from .handlers_test import Handlers_test
-from ycmd.tests.test_utils import DummyCompleter
 from mock import patch
+from nose.tools import eq_
 
+from ycmd.tests import SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, DummyCompleter, PatchCompleter
 
-class Subcommands_test( Handlers_test ):
 
-  @patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap',
-          return_value = { 'A': lambda x: x,
-                           'B': lambda x: x,
-                           'C': lambda x: x } )
-  def Basic_test( self, *args ):
-    with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
-      subcommands_data = self._BuildRequest(
-          completer_target = 'dummy_filetype' )
+@SharedYcmd
+@patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap',
+        return_value = { 'A': lambda x: x,
+                         'B': lambda x: x,
+                         'C': lambda x: x } )
+def Subcommands_Basic_test( app, *args ):
+  with PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+    subcommands_data = BuildRequest( completer_target = 'dummy_filetype' )
 
-      eq_( [ 'A', 'B', 'C' ],
-           self._app.post_json( '/defined_subcommands',
-                                subcommands_data ).json )
+    eq_( [ 'A', 'B', 'C' ],
+         app.post_json( '/defined_subcommands', subcommands_data ).json )
 
 
-  @patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap',
-          return_value = { 'A': lambda x: x,
-                           'B': lambda x: x,
-                           'C': lambda x: x } )
-  def NoExplicitCompleterTargetSpecified_test( self, *args ):
-    with self.PatchCompleter( DummyCompleter, 'dummy_filetype' ):
-      subcommands_data = self._BuildRequest( filetype = 'dummy_filetype' )
+@SharedYcmd
+@patch( 'ycmd.tests.test_utils.DummyCompleter.GetSubcommandsMap',
+        return_value = { 'A': lambda x: x,
+                         'B': lambda x: x,
+                         'C': lambda x: x } )
+def Subcommands_NoExplicitCompleterTargetSpecified_test( app, *args ):
+  with PatchCompleter( DummyCompleter, 'dummy_filetype' ):
+    subcommands_data = BuildRequest( filetype = 'dummy_filetype' )
 
-      eq_( [ 'A', 'B', 'C' ],
-           self._app.post_json( '/defined_subcommands',
-                                subcommands_data ).json )
+    eq_( [ 'A', 'B', 'C' ],
+         app.post_json( '/defined_subcommands', subcommands_data ).json )
diff --git a/ycmd/tests/test_utils.py b/ycmd/tests/test_utils.py
--- a/ycmd/tests/test_utils.py
+++ b/ycmd/tests/test_utils.py
@@ -27,11 +27,17 @@
 from builtins import *  # noqa
 
 from future.utils import PY2
+from hamcrest import contains_string, has_entry, has_entries
+from mock import patch
+from webtest import TestApp
+import bottle
+import contextlib
+
+from ycmd import handlers, user_options_store
 from ycmd.completers.completer import Completer
 from ycmd.responses import BuildCompletionData
 from ycmd.utils import OnMac, OnWindows
 import ycm_core
-import os.path
 
 try:
   from unittest import skipIf
@@ -76,9 +82,64 @@ def BuildRequest( **kwargs ):
   return request
 
 
-def PathToTestFile( *args ):
-  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
-  return os.path.join( dir_of_current_script, 'testdata', *args )
+def ErrorMatcher( cls, msg = None ):
+  """ Returns a hamcrest matcher for a server exception response """
+  entry = { 'exception': has_entry( 'TYPE', cls.__name__ ) }
+
+  if msg:
+    entry.update( { 'message': msg } )
+
+  return has_entries( entry )
+
+
+def CompletionEntryMatcher( insertion_text,
+                            extra_menu_info = None,
+                            extra_params = None ):
+  match = { 'insertion_text': insertion_text }
+
+  if extra_menu_info:
+    match.update( { 'extra_menu_info': extra_menu_info } )
+
+  if extra_params:
+    match.update( extra_params )
+
+  return has_entries( match )
+
+
+def CompletionLocationMatcher( location_type, value ):
+  return has_entry( 'extra_data',
+                    has_entry( 'location',
+                               has_entry( location_type, value ) ) )
+
+
+def MessageMatcher( msg ):
+  return has_entry( 'message', contains_string( msg ) )
+
+
+@contextlib.contextmanager
+def PatchCompleter( completer, filetype ):
+  user_options = handlers._server_state._user_options
+  with patch.dict( 'ycmd.handlers._server_state._filetype_completers',
+                   { filetype: completer( user_options ) } ):
+    yield
+
+
+@contextlib.contextmanager
+def UserOption( key, value ):
+  try:
+    current_options = dict( user_options_store.GetAll() )
+    user_options = current_options.copy()
+    user_options.update( { key: value } )
+    handlers.UpdateUserOptions( user_options )
+    yield
+  finally:
+    handlers.UpdateUserOptions( current_options )
+
+
+def SetUpApp():
+  bottle.debug( True )
+  handlers.SetServerStateToDefaults()
+  return TestApp( handlers.app )
 
 
 class DummyCompleter( Completer ):
diff --git a/ycmd/tests/typescript/__init__.py b/ycmd/tests/typescript/__init__.py
--- a/ycmd/tests/typescript/__init__.py
+++ b/ycmd/tests/typescript/__init__.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2016 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from future import standard_library
+standard_library.install_aliases()
+from builtins import *  # noqa
+
+import functools
+import os
+
+from ycmd.tests.test_utils import SetUpApp
+
+shared_app = None
+
+
+def PathToTestFile( *args ):
+  dir_of_current_script = os.path.dirname( os.path.abspath( __file__ ) )
+  return os.path.join( dir_of_current_script, 'testdata', *args )
+
+
+def setUpPackage():
+  """Initializes the ycmd server as a WebTest application that will be shared
+  by all tests using the SharedYcmd decorator in this package. Additional
+  configuration that is common to these tests, like starting a semantic
+  subserver, should be done here."""
+  global shared_app
+
+  shared_app = SetUpApp()
+
+
+def SharedYcmd( test ):
+  """Defines a decorator to be attached to tests of this package. This decorator
+  passes the shared ycmd application as a parameter.
+
+  Do NOT attach it to test generators but directly to the yielded tests."""
+  global shared_app
+
+  @functools.wraps( test )
+  def Wrapper( *args, **kwargs ):
+    return test( shared_app, *args, **kwargs )
+  return Wrapper
diff --git a/ycmd/tests/typescript/get_completions_test.py b/ycmd/tests/typescript/get_completions_test.py
--- a/ycmd/tests/typescript/get_completions_test.py
+++ b/ycmd/tests/typescript/get_completions_test.py
@@ -24,67 +24,67 @@
 from builtins import *  # noqa
 
 from hamcrest import assert_that, contains_inanyorder, has_entries
-from .typescript_handlers_test import Typescript_Handlers_test
-from ycmd.utils import ReadFile
 from mock import patch
 
+from ycmd.tests.typescript import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, CompletionEntryMatcher
+from ycmd.utils import ReadFile
+
 
-class TypeScript_GetCompletions_test( Typescript_Handlers_test ):
+def RunTest( app, test ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
 
-  def _RunTest( self, test ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
 
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
+  app.post_json( '/event_notification', event_data )
 
-    self._app.post_json( '/event_notification', event_data )
+  completion_data = BuildRequest( filepath = filepath,
+                                  filetype = 'typescript',
+                                  contents = contents,
+                                  force_semantic = True,
+                                  line_num = 12,
+                                  column_num = 6 )
 
-    completion_data = self._BuildRequest( filepath = filepath,
-                                          filetype = 'typescript',
-                                          contents = contents,
-                                          force_semantic = True,
-                                          line_num = 12,
-                                          column_num = 6 )
+  response = app.post_json( '/completions', completion_data )
 
-    response = self._app.post_json( '/completions', completion_data )
-    assert_that( response.json, test[ 'expect' ][ 'data' ] )
+  assert_that( response.json, test[ 'expect' ][ 'data' ] )
 
 
-  def Basic_test( self ):
-    self._RunTest( {
-      'expect': {
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            self.CompletionEntryMatcher(
-              'methodA',
-              'methodA (method) Foo.methodA(): void' ),
-            self.CompletionEntryMatcher(
-              'methodB',
-              'methodB (method) Foo.methodB(): void' ),
-            self.CompletionEntryMatcher(
-              'methodC',
-              'methodC (method) Foo.methodC(): void' ),
-          )
-        } )
-      }
-    } )
+@SharedYcmd
+def GetCompletions_Basic_test( app ):
+  RunTest( app, {
+    'expect': {
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          CompletionEntryMatcher( 'methodA', extra_params = {
+            'menu_text': 'methodA (method) Foo.methodA(): void' } ),
+          CompletionEntryMatcher( 'methodB', extra_params = {
+            'menu_text': 'methodB (method) Foo.methodB(): void' } ),
+          CompletionEntryMatcher( 'methodC', extra_params = {
+            'menu_text': 'methodC (method) Foo.methodC(): void' } ),
+        )
+      } )
+    }
+  } )
 
 
-  @patch( 'ycmd.completers.typescript.'
-            'typescript_completer.MAX_DETAILED_COMPLETIONS',
-          2 )
-  def MaxDetailedCompletion_test( self ):
-    self._RunTest( {
-      'expect': {
-        'data': has_entries( {
-          'completions': contains_inanyorder(
-            self.CompletionEntryMatcher( 'methodA' ),
-            self.CompletionEntryMatcher( 'methodB' ),
-            self.CompletionEntryMatcher( 'methodC' )
-          )
-        } )
-      }
-    } )
+@SharedYcmd
+@patch( 'ycmd.completers.typescript.'
+          'typescript_completer.MAX_DETAILED_COMPLETIONS',
+        2 )
+def GetCompletions_MaxDetailedCompletion_test( app ):
+  RunTest( app, {
+    'expect': {
+      'data': has_entries( {
+        'completions': contains_inanyorder(
+          CompletionEntryMatcher( 'methodA' ),
+          CompletionEntryMatcher( 'methodB' ),
+          CompletionEntryMatcher( 'methodC' )
+        )
+      } )
+    }
+  } )
diff --git a/ycmd/tests/typescript/subcommands_test.py b/ycmd/tests/typescript/subcommands_test.py
--- a/ycmd/tests/typescript/subcommands_test.py
+++ b/ycmd/tests/typescript/subcommands_test.py
@@ -24,201 +24,202 @@
 from builtins import *  # noqa
 
 from hamcrest import assert_that, has_items, has_entries
-from .typescript_handlers_test import Typescript_Handlers_test
-from ycmd.utils import ReadFile
-
 
-class TypeScript_Subcommands_test( Typescript_Handlers_test ):
-
-  def GetType_Basic_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
+from ycmd.tests.typescript import PathToTestFile, SharedYcmd
+from ycmd.tests.test_utils import BuildRequest, ErrorMatcher, MessageMatcher
+from ycmd.utils import ReadFile
 
-    gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                       command_arguments = [ 'GetType' ],
-                                       line_num = 12,
-                                       column_num = 1,
-                                       contents = contents,
-                                       filetype = 'typescript',
-                                       filepath = filepath )
 
-    response = self._app.post_json( '/run_completer_command',
-                                    gettype_data ).json
-    assert_that( response, self._MessageMatcher( 'var foo: Foo' ) )
-
-
-  def GetType_HasNoType_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                       command_arguments = [ 'GetType' ],
-                                       line_num = 2,
-                                       column_num = 1,
-                                       contents = contents,
-                                       filetype = 'typescript',
-                                       filepath = filepath )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    gettype_data,
-                                    expect_errors = True ).json
-    assert_that( response,
-                 self._ErrorMatcher( RuntimeError, 'No content available.' ) )
-
-
-  def GetDoc_Method_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                       command_arguments = [ 'GetDoc' ],
-                                       line_num = 29,
-                                       column_num = 9,
-                                       contents = contents,
-                                       filetype = 'typescript',
-                                       filepath = filepath )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    gettype_data ).json
-    assert_that( response,
-                 has_entries( {
-                   'detailed_info': '(method) Bar.testMethod(): void\n\n'
-                                    'Method documentation'
-                 } ) )
-
-
-  def GetDoc_Class_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    gettype_data = self._BuildRequest( completer_target = 'filetype_default',
-                                       command_arguments = [ 'GetDoc' ],
-                                       line_num = 32,
-                                       column_num = 2,
-                                       contents = contents,
-                                       filetype = 'typescript',
-                                       filepath = filepath )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    gettype_data ).json
-    assert_that( response,
-                 has_entries( {
-                   'detailed_info': 'class Bar\n\n'
-                                    'Class documentation\n\n'
-                                    'Multi-line'
-                 } ) )
-
-
-  def GoToReferences_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    references_data = self._BuildRequest(
-      completer_target = 'filetype_default',
-      command_arguments = [ 'GoToReferences' ],
-      line_num = 28,
-      column_num = 6,
-      contents = contents,
-      filetype = 'typescript',
-      filepath = filepath )
-
-    expected = has_items(
-      has_entries( { 'description': 'var bar = new Bar();',
-                     'line_num'   : 28,
-                     'column_num' : 5 } ),
-      has_entries( { 'description': 'bar.testMethod();',
-                     'line_num'   : 29,
-                     'column_num' : 1 } ) )
-    actual = self._app.post_json( '/run_completer_command',
-                                  references_data ).json
-    assert_that( actual, expected )
-
-
-  def GoTo_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                     filetype = 'typescript',
-                                     contents = contents,
-                                     event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ 'GoToDefinition' ],
-                                    line_num = 29,
-                                    column_num = 9,
-                                    contents = contents,
-                                    filetype = 'typescript',
-                                    filepath = filepath )
-
-    response = self._app.post_json( '/run_completer_command', goto_data ).json
-    assert_that( response,
-                 has_entries( {
-                   'filepath': filepath,
-                   'line_num': 25,
-                   'column_num': 3,
-                 } ) )
-
-
-  def GoTo_Fail_test( self ):
-    filepath = self._PathToTestFile( 'test.ts' )
-    contents = ReadFile( filepath )
-
-    event_data = self._BuildRequest( filepath = filepath,
-                                      filetype = 'typescript',
-                                      contents = contents,
-                                      event_name = 'BufferVisit' )
-
-    self._app.post_json( '/event_notification', event_data )
-
-    goto_data = self._BuildRequest( completer_target = 'filetype_default',
-                                    command_arguments = [ 'GoToDefinition' ],
-                                    line_num = 30,
-                                    column_num = 6,
-                                    contents = contents,
-                                    filetype = 'typescript',
-                                    filepath = filepath )
-
-    response = self._app.post_json( '/run_completer_command',
-                                    goto_data,
-                                    expect_errors = True ).json
-    assert_that( response,
-                 self._ErrorMatcher( RuntimeError,
-                                     'Could not find definition' ) )
+@SharedYcmd
+def Subcommands_GetType_Basic_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  gettype_data = BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'GetType' ],
+                               line_num = 12,
+                               column_num = 1,
+                               contents = contents,
+                               filetype = 'typescript',
+                               filepath = filepath )
+
+  response = app.post_json( '/run_completer_command', gettype_data ).json
+  assert_that( response, MessageMatcher( 'var foo: Foo' ) )
+
+
+@SharedYcmd
+def Subcommands_GetType_HasNoType_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  gettype_data = BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'GetType' ],
+                               line_num = 2,
+                               column_num = 1,
+                               contents = contents,
+                               filetype = 'typescript',
+                               filepath = filepath )
+
+  response = app.post_json( '/run_completer_command',
+                            gettype_data,
+                            expect_errors = True ).json
+  assert_that( response,
+               ErrorMatcher( RuntimeError, 'No content available.' ) )
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Method_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  gettype_data = BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'GetDoc' ],
+                               line_num = 29,
+                               column_num = 9,
+                               contents = contents,
+                               filetype = 'typescript',
+                               filepath = filepath )
+
+  response = app.post_json( '/run_completer_command', gettype_data ).json
+  assert_that( response,
+               has_entries( {
+                 'detailed_info': '(method) Bar.testMethod(): void\n\n'
+                                  'Method documentation'
+               } ) )
+
+
+@SharedYcmd
+def Subcommands_GetDoc_Class_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  gettype_data = BuildRequest( completer_target = 'filetype_default',
+                               command_arguments = [ 'GetDoc' ],
+                               line_num = 32,
+                               column_num = 2,
+                               contents = contents,
+                               filetype = 'typescript',
+                               filepath = filepath )
+
+  response = app.post_json( '/run_completer_command', gettype_data ).json
+  assert_that( response,
+               has_entries( {
+                 'detailed_info': 'class Bar\n\n'
+                                  'Class documentation\n\n'
+                                  'Multi-line'
+               } ) )
+
+
+@SharedYcmd
+def Subcommands_GoToReferences_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  references_data = BuildRequest( completer_target = 'filetype_default',
+                                  command_arguments = [ 'GoToReferences' ],
+                                  line_num = 28,
+                                  column_num = 6,
+                                  contents = contents,
+                                  filetype = 'typescript',
+                                  filepath = filepath )
+
+  expected = has_items(
+    has_entries( { 'description': 'var bar = new Bar();',
+                   'line_num'   : 28,
+                   'column_num' : 5 } ),
+    has_entries( { 'description': 'bar.testMethod();',
+                   'line_num'   : 29,
+                   'column_num' : 1 } ) )
+  actual = app.post_json( '/run_completer_command', references_data ).json
+  assert_that( actual, expected )
+
+
+@SharedYcmd
+def Subcommands_GoTo_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ 'GoToDefinition' ],
+                            line_num = 29,
+                            column_num = 9,
+                            contents = contents,
+                            filetype = 'typescript',
+                            filepath = filepath )
+
+  response = app.post_json( '/run_completer_command', goto_data ).json
+  assert_that( response,
+               has_entries( {
+                 'filepath': filepath,
+                 'line_num': 25,
+                 'column_num': 3,
+               } ) )
+
+
+@SharedYcmd
+def Subcommands_GoTo_Fail_test( app ):
+  filepath = PathToTestFile( 'test.ts' )
+  contents = ReadFile( filepath )
+
+  event_data = BuildRequest( filepath = filepath,
+                             filetype = 'typescript',
+                             contents = contents,
+                             event_name = 'BufferVisit' )
+
+  app.post_json( '/event_notification', event_data )
+
+  goto_data = BuildRequest( completer_target = 'filetype_default',
+                            command_arguments = [ 'GoToDefinition' ],
+                            line_num = 30,
+                            column_num = 6,
+                            contents = contents,
+                            filetype = 'typescript',
+                            filepath = filepath )
+
+  response = app.post_json( '/run_completer_command',
+                            goto_data,
+                            expect_errors = True ).json
+  assert_that( response,
+               ErrorMatcher( RuntimeError, 'Could not find definition' ) )
diff --git a/ycmd/tests/typescript/typescript_handlers_test.py b/ycmd/tests/typescript/typescript_handlers_test.py
deleted file mode 100644
--- a/ycmd/tests/typescript/typescript_handlers_test.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# Copyright (C) 2015 ycmd contributors
-#
-# This file is part of ycmd.
-#
-# ycmd is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# ycmd is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with ycmd.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from __future__ import division
-from __future__ import absolute_import
-from future import standard_library
-standard_library.install_aliases()
-from builtins import *  # noqa
-
-from ..handlers_test import Handlers_test
-
-
-class Typescript_Handlers_test( Handlers_test ):
-
-  def __init__( self ):
-    self._file = __file__
-
-
-  def CompletionEntryMatcher( self, insertion_text, menu_text = None ):
-    if not menu_text:
-      menu_text = insertion_text
-
-    extra_params = { 'menu_text': menu_text }
-    return self._CompletionEntryMatcher( insertion_text,
-                                         extra_params = extra_params )
diff --git a/ycmd/tests/utils_test.py b/ycmd/tests/utils_test.py
--- a/ycmd/tests/utils_test.py
+++ b/ycmd/tests/utils_test.py
@@ -31,7 +31,8 @@
 from mock import patch, call
 from nose.tools import eq_, ok_
 from ycmd import utils
-from ycmd.tests.test_utils import PathToTestFile, Py2Only, Py3Only, WindowsOnly
+from ycmd.tests.test_utils import Py2Only, Py3Only, WindowsOnly
+from ycmd.tests import PathToTestFile
 
 # NOTE: isinstance() vs type() is carefully used in this test file. Before
 # changing things here, read the comments in utils.ToBytes.