diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..42cf824
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,71 @@
+name: CI
+
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - '*'
+
+jobs:
+  linting:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+
+    steps:
+    - uses: actions/checkout@v1
+      with:
+        fetch-depth: 1
+
+    - uses: actions/setup-python@v2
+      with:
+        python-version: 3.8
+
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -U requests
+        make install-pre-commit
+        make install-deps
+
+    - name: Run pre-commit
+      run: |
+        pre-commit run -a -v
+
+    - name: Run mypy
+      run: |
+        mypy --config-file=./mypy.ini -p responses
+
+  tests:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9']
+        requests-version: ['"requests>=2.0,<3.0"', '-U requests']
+
+    steps:
+    - uses: actions/checkout@v1
+      with:
+        fetch-depth: 1
+
+    - uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install ${{ matrix.requests-version }}
+        make install-deps
+
+    - name: Run Pytest
+      run: |
+        # Run test
+        pytest . --cov-report term-missing --cov .
+
+    - name: Code Coverage Report
+      if: success()
+      uses: codecov/codecov-action@v1
diff --git a/.travis.yml b/.travis.yml
index 44ca124..eae8538 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,13 +2,7 @@ os: linux
 dist: xenial
 language: python
 python:
-- '2.7'
-- '3.5'
-- '3.6'
-- '3.7'
 - '3.8'
-- '3.9'
-- nightly
 
 cache:
   directories:
@@ -16,9 +10,7 @@ cache:
 
 env:
   jobs:
-  - REQUESTS="requests>=2.0,<3.0"
   - REQUESTS="-U requests"
-  - REQUESTS="-e git+git://github.com/requests/requests.git#egg=requests"
   global:
   - PIP_DOWNLOAD_CACHE=".pip_download_cache"
 
@@ -34,7 +26,6 @@ install:
 
 script:
 - make
-- if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then pre-commit run -a -v; fi
 - pytest . --cov-report term-missing  --cov . --cov-report="xml:.artifacts/coverage.xml"
   --junit-xml=".artifacts/junit.xml"
 
@@ -47,26 +38,3 @@ deploy:
     secure: SejRAkh567ae8HNgLMWebpPwOxn2+wfrKWHkm8JTmCJcxKIQmfbPlUJPAspJ9HF7kBB1MzkkqNkXG184VEgH1l1JqUNyZKqrk+y4R3s+jjJ+Yqepm3SeAiq5FceS8BwCegO9L3po299NtdWFHllydVLLL+Rf3ZnCQOl5YT1tzzk=
   on:
     tags: true
-
-after_success:
-- codecov -e REQUESTS -e TRAVIS_PYTHON_VERSION
-- npm install -g @zeus-ci/cli
-- zeus upload -t "text/x-pycodestyle" .artifacts/flake8.log
-- zeus upload -t "text/xml+xunit" .artifacts/*junit.xml
-- zeus upload -t "text/xml+coverage" .artifacts/*coverage.xml
-
-after_failure:
-- npm install -g @zeus-ci/cli
-- zeus upload -t "text/x-pycodestyle" .artifacts/flake8.log
-- zeus upload -t "text/xml+xunit" .artifacts/*junit.xml
-- zeus upload -t "text/xml+coverage" .artifacts/*coverage.xml
-
-notifications:
-  webhooks:
-    urls:
-    - https://zeus.ci/hooks/4f39f396-3820-11e8-982a-0a580a28001b/public/provider/travis/webhook
-    on_success: always
-    on_failure: always
-    on_start: always
-    on_cancel: always
-    on_error: always
diff --git a/README.rst b/README.rst
index 70e911b..6b62102 100644
--- a/README.rst
+++ b/README.rst
@@ -475,6 +475,10 @@ replaced.
         assert resp.json() == {'data': 2}
 
 
+The ``upsert`` function allows a previously registered ``response`` to be
+changed like ``replace``. If the response is registered, the ``upsert`` function
+will registered it like ``add``.
+
 ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
 matched responses from the registered list.
 
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..8e109ff
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,37 @@
+[mypy]
+show_column_numbers=True
+show_error_codes = True
+
+disallow_any_unimported=True
+disallow_any_expr=False
+disallow_any_decorated=True
+disallow_any_explicit=False
+disallow_any_generics=True
+disallow_subclassing_any=True
+
+disallow_untyped_calls=True
+disallow_untyped_defs=True
+disallow_incomplete_defs=True
+check_untyped_defs=True
+disallow_untyped_decorators=True
+
+no_implicit_optional=True
+strict_optional=True
+
+warn_redundant_casts=True
+warn_unused_ignores=True
+warn_no_return=True
+warn_return_any=False
+warn_unreachable=False
+
+strict_equality=True
+
+ignore_missing_imports=False
+
+[mypy-urllib3.*]
+ignore_missing_imports=True
+
+[mypy-responses.test_responses]
+disallow_untyped_defs=False
+disallow_untyped_calls=False
+disallow_any_decorated=False
diff --git a/responses/__init__.py b/responses/__init__.py
index 24d60e1..7e4a6e5 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -621,6 +621,20 @@ class RequestsMock(object):
             raise ValueError("Response is not registered for URL %s" % url)
         self._matches[index] = response
 
+    def upsert(self, method_or_response=None, url=None, body="", *args, **kwargs):
+        """
+        Replaces a response previously added using ``add()``, or adds the response
+        if no response exists.  Responses are matched using ``method``and ``url``.
+        The first matching response is replaced.
+
+        >>> responses.add(responses.GET, 'http://example.org', json={'data': 1})
+        >>> responses.upsert(responses.GET, 'http://example.org', json={'data': 2})
+        """
+        try:
+            self.replace(method_or_response, url, body, *args, **kwargs)
+        except ValueError:
+            self.add(method_or_response, url, body, *args, **kwargs)
+
     def add_callback(
         self, method, url, callback, match_querystring=False, content_type="text/plain"
     ):
@@ -802,6 +816,7 @@ __all__ = [
     "start",
     "stop",
     "target",
+    "upsert",
 ]
 
 activate = _default_mock.activate
@@ -826,3 +841,4 @@ response_callback = _default_mock.response_callback
 start = _default_mock.start
 stop = _default_mock.stop
 target = _default_mock.target
+upsert = _default_mock.upsert
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index 9a36e88..5823a60 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -159,7 +159,7 @@ class RequestsMock:
     ) -> None: ...
     def reset(self) -> None: ...
     add: _Add
-    def add_passthru(self, prefix: str) -> None: ...
+    add_passthru: _AddPassthru
     def remove(
         self,
         method_or_response: Optional[Union[str, Response]] = ...,
@@ -184,7 +184,7 @@ class _Activate(Protocol):
 class _Add(Protocol):
     def __call__(
         self,
-        method: Optional[str] = ...,
+        method: Optional[Union[str, BaseResponse]] = ...,
         url: Optional[Union[Pattern[str], str]] = ...,
         body: _Body = ...,
         json: Optional[Any] = ...,
@@ -207,17 +207,22 @@ class _AddCallback(Protocol):
         content_type: Optional[str] = ...,
     ) -> None: ...
 
+class _AddPassthru(Protocol):
+    def __call__(
+        self, prefix: Union[Pattern[str], str]
+    ) -> None: ...
+
 class _Remove(Protocol):
     def __call__(
         self,
-        method_or_response: Optional[Union[str, Response]] = ...,
+        method_or_response: Optional[Union[str, BaseResponse]] = ...,
         url: Optional[Union[Pattern[str], str]] = ...,
     ) -> None: ...
 
 class _Replace(Protocol):
     def __call__(
         self,
-        method_or_response: Optional[Union[str, Response]] = ...,
+        method_or_response: Optional[Union[str, BaseResponse]] = ...,
         url: Optional[Union[Pattern[str], str]] = ...,
         body: _Body = ...,
         json: Optional[Any] = ...,
@@ -233,7 +238,7 @@ class _Replace(Protocol):
 activate: _Activate
 add: _Add
 add_callback: _AddCallback
-add_passthru: Callable[[str], None]
+add_passthru: _AddPassthru
 assert_all_requests_are_fired: bool
 assert_call_count: Callable[[str, int], bool]
 calls: CallList
diff --git a/setup.py b/setup.py
index 399f43e..2661439 100644
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,7 @@ tests_require = [
     "pytest-cov",
     "pytest-localserver",
     "flake8",
+    "mypy; python_version >= '3.5'",
 ]
 
 extras_require = {"tests": tests_require}

diff --git a/responses/test_responses.py b/responses/test_responses.py
index 0584a3c..fdab8da 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -16,7 +16,7 @@ from responses import BaseResponse, Response
 try:
     from mock import patch, Mock
 except ImportError:
-    from unittest.mock import patch, Mock
+    from unittest.mock import patch, Mock  # type: ignore
 
 
 def assert_reset():
@@ -34,6 +34,14 @@ def assert_response(resp, body=None, content_type="text/plain"):
     assert resp.text == body
 
 
+def assert_params(resp, expected):
+    assert hasattr(resp, "request"), "Missing request"
+    assert hasattr(
+        resp.request, "params"
+    ), "Missing params on request that responses should add"
+    assert getattr(resp.request, "params") == expected, "Incorrect parameters"
+
+
 def test_response():
     @responses.activate
     def run():
@@ -164,6 +172,77 @@ def test_replace_response_object_error():
     assert_reset()
 
 
+@pytest.mark.parametrize(
+    "original,replacement",
+    [
+        ("http://example.com/two", "http://example.com/two"),
+        (
+            Response(method=responses.GET, url="http://example.com/two"),
+            Response(
+                method=responses.GET, url="http://example.com/two", body="testtwo"
+            ),
+        ),
+        (
+            re.compile(r"http://example\.com/two"),
+            re.compile(r"http://example\.com/two"),
+        ),
+    ],
+)
+def test_upsert_replace(original, replacement):
+    @responses.activate
+    def run():
+        responses.add(responses.GET, "http://example.com/one", body="test1")
+
+        if isinstance(original, BaseResponse):
+            responses.add(original)
+        else:
+            responses.add(responses.GET, original, body="test2")
+
+        if isinstance(replacement, BaseResponse):
+            responses.upsert(replacement)
+        else:
+            responses.upsert(responses.GET, replacement, body="testtwo")
+
+        resp = requests.get("http://example.com/two")
+        assert_response(resp, "testtwo")
+
+    run()
+    assert_reset()
+
+
+@pytest.mark.parametrize(
+    "original,replacement",
+    [
+        ("http://example.com/two", "http://example.com/two"),
+        (
+            Response(method=responses.GET, url="http://example.com/two"),
+            Response(
+                method=responses.GET, url="http://example.com/two", body="testtwo"
+            ),
+        ),
+        (
+            re.compile(r"http://example\.com/two"),
+            re.compile(r"http://example\.com/two"),
+        ),
+    ],
+)
+def test_upsert_add(original, replacement):
+    @responses.activate
+    def run():
+        responses.add(responses.GET, "http://example.com/one", body="test1")
+
+        if isinstance(replacement, BaseResponse):
+            responses.upsert(replacement)
+        else:
+            responses.upsert(responses.GET, replacement, body="testtwo")
+
+        resp = requests.get("http://example.com/two")
+        assert_response(resp, "testtwo")
+
+    run()
+    assert_reset()
+
+
 def test_remove():
     @responses.activate
     def run():
@@ -802,7 +881,7 @@ def test_response_callback():
             resp = requests.get("http://example.com")
             assert resp.text == "test"
             assert hasattr(resp, "_is_mocked")
-            assert resp._is_mocked is True
+            assert getattr(resp, "_is_mocked") is True
 
     run()
     assert_reset()
@@ -813,8 +892,8 @@ def test_response_filebody():
 
     def run():
         with responses.RequestsMock() as m:
-            with open("README.rst", "rb") as out:
-                m.add(responses.GET, "http://example.com", body=out, stream=True)
+            with open("README.rst", "r") as out:
+                m.add(responses.GET, "http://example.com", body=out.read(), stream=True)
                 resp = requests.get("http://example.com")
             with open("README.rst", "r") as out:
                 assert resp.text == out.read()
@@ -963,7 +1042,7 @@ def test_handles_buffered_reader_body():
 
     @responses.activate
     def run():
-        responses.add(responses.GET, url, body=BufferedReader(BytesIO(b"test")))
+        responses.add(responses.GET, url, body=BufferedReader(BytesIO(b"test")))  # type: ignore
 
         resp = requests.get(url)
 
@@ -1161,11 +1240,11 @@ def test_request_param(url):
         )
         resp = requests.get(url, params=params)
         assert_response(resp, "test")
-        assert resp.request.params == params
+        assert_params(resp, params)
 
         resp = requests.get(url)
         assert_response(resp, "test")
-        assert resp.request.params == {}
+        assert_params(resp, {})
 
     run()
     assert_reset()
@@ -1183,7 +1262,7 @@ def test_request_param_with_multiple_values_for_the_same_key():
         )
         resp = requests.get(url, params=params)
         assert_response(resp, "test")
-        assert resp.request.params == params
+        assert_params(resp, params)
 
     run()
     assert_reset()

Add an "update" (upsert) method for adding responses to RequestMock
RequestMock currently has add, remove and replace.

However replace doesn't work if the result does not already exist, meaning you need to write a lot of remove/add code (which does work if the result does not already exist) when updating a RequestMock.

Ideally there would be an "upsert" type method which would wrap up this type of operation, leading to tighter test code.

Due to the ordering of RequestMock, a directionality would probably be needed for common use cases - i.e. add to the end if does not exist, or add to the beginning if does not exist.
responses/test_responses.py::test_upsert_replace[http://example.com/two-http://example.com/two] responses/test_responses.py::test_upsert_replace[original1-replacement1] responses/test_responses.py::test_upsert_replace[http://example\\.com/two-http://example\\.com/two] responses/test_responses.py::test_upsert_add[http://example.com/two-http://example.com/two] responses/test_responses.py::test_upsert_add[original1-replacement1] responses/test_responses.py::test_upsert_add[http://example\\.com/two-http://example\\.com/two]
responses/test_responses.py::test_response responses/test_responses.py::test_response_encoded responses/test_responses.py::test_response_with_instance responses/test_responses.py::test_replace[http://example.com/two-http://example.com/two] responses/test_responses.py::test_replace[original1-replacement1] responses/test_responses.py::test_replace[http://example\\.com/two-http://example\\.com/two] responses/test_responses.py::test_replace_error[http://example.com/one-http://example\\.com/one] responses/test_responses.py::test_replace_error[http://example\\.com/one-http://example.com/one] responses/test_responses.py::test_replace_response_object_error responses/test_responses.py::test_remove responses/test_responses.py::test_response_equality[args10-kwargs10-args20-kwargs20-True] responses/test_responses.py::test_response_equality[args11-kwargs11-args21-kwargs21-False] responses/test_responses.py::test_response_equality[args12-kwargs12-args22-kwargs22-False] responses/test_responses.py::test_response_equality[args13-kwargs13-args23-kwargs23-True] responses/test_responses.py::test_response_equality_different_objects responses/test_responses.py::test_connection_error responses/test_responses.py::test_match_querystring responses/test_responses.py::test_match_empty_querystring responses/test_responses.py::test_match_querystring_error responses/test_responses.py::test_match_querystring_regex responses/test_responses.py::test_match_querystring_error_regex responses/test_responses.py::test_match_querystring_auto_activates responses/test_responses.py::test_match_querystring_missing_key responses/test_responses.py::test_accept_string_body responses/test_responses.py::test_accept_json_body responses/test_responses.py::test_no_content_type responses/test_responses.py::test_arbitrary_status_code responses/test_responses.py::test_throw_connection_error_explicit responses/test_responses.py::test_callback responses/test_responses.py::test_callback_exception_result responses/test_responses.py::test_callback_exception_body responses/test_responses.py::test_callback_no_content_type responses/test_responses.py::test_callback_content_type_dict responses/test_responses.py::test_callback_content_type_tuple responses/test_responses.py::test_regular_expression_url responses/test_responses.py::test_custom_adapter responses/test_responses.py::test_responses_as_context_manager responses/test_responses.py::test_activate_doesnt_change_signature responses/test_responses.py::test_activate_mock_interaction responses/test_responses.py::test_activate_doesnt_change_signature_with_return_type responses/test_responses.py::test_activate_doesnt_change_signature_for_method responses/test_responses.py::test_response_cookies responses/test_responses.py::test_response_secure_cookies responses/test_responses.py::test_response_cookies_multiple responses/test_responses.py::test_response_callback responses/test_responses.py::test_response_filebody responses/test_responses.py::test_assert_all_requests_are_fired responses/test_responses.py::test_allow_redirects_samehost responses/test_responses.py::test_handles_unicode_querystring responses/test_responses.py::test_handles_unicode_url responses/test_responses.py::test_handles_unicode_body responses/test_responses.py::test_handles_buffered_reader_body responses/test_responses.py::test_headers responses/test_responses.py::test_legacy_adding_headers responses/test_responses.py::test_multiple_responses responses/test_responses.py::test_multiple_urls responses/test_responses.py::test_multiple_methods responses/test_responses.py::test_passthru responses/test_responses.py::test_passthru_regex responses/test_responses.py::test_method_named_param responses/test_responses.py::test_passthru_unicode responses/test_responses.py::test_custom_target responses/test_responses.py::test_cookies_from_headers responses/test_responses.py::test_request_param[http://example.com] responses/test_responses.py::test_request_param[http://example.com/some/path] responses/test_responses.py::test_request_param[http://example.com/other/path/] responses/test_responses.py::test_request_param_with_multiple_values_for_the_same_key responses/test_responses.py::test_assert_call_count[http://example.com] responses/test_responses.py::test_assert_call_count[http://example.com?hello=world] responses/test_responses.py::test_request_matches_post_params responses/test_responses.py::test_request_matches_empty_body responses/test_responses.py::test_fail_request_error