diff --git a/lib/ansible/module_utils/acme.py b/lib/ansible/module_utils/acme.py
index 3e776fa169..0217786e9f 100644
--- a/lib/ansible/module_utils/acme.py
+++ b/lib/ansible/module_utils/acme.py
@@ -795,7 +795,7 @@ def cryptography_get_csr_domains(module, csr_filename):
     return domains
 
 
-def cryptography_get_cert_days(module, cert_file):
+def cryptography_get_cert_days(module, cert_file, now=None):
     '''
     Return the days the certificate in cert_file remains valid and -1
     if the file was not found. If cert_file contains more than one
@@ -808,7 +808,8 @@ def cryptography_get_cert_days(module, cert_file):
         cert = cryptography.x509.load_pem_x509_certificate(read_file(cert_file), _cryptography_backend)
     except Exception as e:
         raise ModuleFailException('Cannot parse certificate {0}: {1}'.format(cert_file, e))
-    now = datetime.datetime.now()
+    if now is None:
+        now = datetime.datetime.now()
     return (cert.not_valid_after - now).days
 
 
diff --git a/test/units/module_utils/test_acme.py b/test/units/module_utils/test_acme.py
new file mode 100644
index 0000000000..92831a370f
--- /dev/null
+++ b/test/units/module_utils/test_acme.py
@@ -0,0 +1,201 @@
+import base64
+import datetime
+import pytest
+
+from mock import MagicMock
+
+
+from ansible.module_utils.acme import (
+    HAS_CURRENT_CRYPTOGRAPHY,
+    nopad_b64,
+    write_file,
+    read_file,
+    pem_to_der,
+    _parse_key_openssl,
+    # _sign_request_openssl,
+    _parse_key_cryptography,
+    # _sign_request_cryptography,
+    cryptography_get_csr_domains,
+    cryptography_get_cert_days,
+)
+
+################################################
+
+NOPAD_B64 = [
+    ("", ""),
+    ("\n", "Cg"),
+    ("123", "MTIz"),
+    ("Lorem?ipsum", "TG9yZW0_aXBzdW0"),
+]
+
+
+@pytest.mark.parametrize("value, result", NOPAD_B64)
+def test_nopad_b64(value, result):
+    assert nopad_b64(value.encode('utf-8')) == result
+
+
+################################################
+
+TEST_TEXT = R"""1234
+5678"""
+
+
+def test_read_file(tmpdir):
+    fn = tmpdir / 'test.txt'
+    fn.write(TEST_TEXT)
+    assert read_file(str(fn), 't') == TEST_TEXT
+    assert read_file(str(fn), 'b') == TEST_TEXT.encode('utf-8')
+
+
+def test_write_file(tmpdir):
+    fn = tmpdir / 'test.txt'
+    module = MagicMock()
+    write_file(module, str(fn), TEST_TEXT.encode('utf-8'))
+    assert fn.read() == TEST_TEXT
+
+
+################################################
+
+TEST_PEM_DERS = [
+    (
+        R"""
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49
+AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A
+mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
+-----END EC PRIVATE KEY-----
+""",
+        base64.b64decode('MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAo'
+                         'GCCqGSM49AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3'
+                         'lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==')
+    )
+]
+
+
+@pytest.mark.parametrize("pem, der", TEST_PEM_DERS)
+def test_pem_to_der(pem, der, tmpdir):
+    fn = tmpdir / 'test.pem'
+    fn.write(pem)
+    assert pem_to_der(str(fn)) == der
+
+
+################################################
+
+TEST_KEYS = [
+    (
+        R"""
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49
+AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A
+mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
+-----END EC PRIVATE KEY-----
+""",
+        {
+            'alg': 'ES256',
+            'hash': 'sha256',
+            'jwk': {
+                'crv': 'P-256',
+                'kty': 'EC',
+                'x': 'AJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68E',
+                'y': 'vEEs4V0egJkNyM2Q4pp001zu14VcpQ0_Ei8xOOPxKZs',
+            },
+            'point_size': 32,
+            'type': 'ec',
+        },
+        R"""
+read EC key
+Private-Key: (256 bit)
+priv:
+    35:9a:8d:4d:0f:ca:16:0a:7a:e9:5f:cb:f9:6e:36:
+    d9:00:bd:ee:c3:93:04:34:d5:b5:c9:f7:bc:db:c4:
+    1e:ba
+pub:
+    04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
+    f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
+    d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
+    e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
+    38:e3:f1:29:9b
+ASN1 OID: prime256v1
+NIST CURVE: P-256
+"""
+    )
+]
+
+
+@pytest.mark.parametrize("pem, result, openssl_output", TEST_KEYS)
+def test_eckeyparse_openssl(pem, result, openssl_output, tmpdir):
+    fn = tmpdir / 'test.key'
+    fn.write(pem)
+    module = MagicMock()
+    module.run_command = MagicMock(return_value=(0, openssl_output, 0))
+    error, key = _parse_key_openssl('openssl', module, key_file=str(fn))
+    assert error is None
+    key.pop('key_file')
+    assert key == result
+
+
+if HAS_CURRENT_CRYPTOGRAPHY:
+    @pytest.mark.parametrize("pem, result, dummy", TEST_KEYS)
+    def test_eckeyparse_cryptography(pem, result, dummy):
+        module = MagicMock()
+        error, key = _parse_key_cryptography(module, key_content=pem)
+        assert error is None
+        key.pop('key_obj')
+        assert key == result
+
+
+################################################
+
+TEST_CSR = R"""
+-----BEGIN NEW CERTIFICATE REQUEST-----
+MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG
+CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs
+4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE
+MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E
+AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl
+FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr
+URnCJfTLr2T3
+-----END NEW CERTIFICATE REQUEST-----
+"""
+
+
+if HAS_CURRENT_CRYPTOGRAPHY:
+    def test_csrdomains_cryptography(tmpdir):
+        fn = tmpdir / 'test.csr'
+        fn.write(TEST_CSR)
+        module = MagicMock()
+        domains = cryptography_get_csr_domains(module, str(fn))
+        assert domains == set(['ansible.com', 'example.com', 'example.org'])
+
+
+################################################
+
+TEST_CERT = R"""
+-----BEGIN CERTIFICATE-----
+MIIBljCCATugAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwthbnNpYmxl
+LmNvbTAeFw0xODExMjUxNTI4MjNaFw0xODExMjYxNTI4MjRaMBYxFDASBgNVBAMT
+C2Fuc2libGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAJz0yAAXAwEm
+OhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8S
+LzE44/Epm6N6MHgwIwYDVR0RBBwwGoILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jn
+MAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/BAUDAweAADATBgNVHSUEDDAKBggrBgEF
+BQcDATAdBgNVHQ4EFgQUmNL9PMzNaUX74owwLFRiGDS3B3MwCgYIKoZIzj0EAwID
+SQAwRgIhALz7Ur96ky0OfM5D9MwFmCg2jccqm/UglGI9+4KeOEIyAiEAwFX4tdll
+QSrd1HY/jMsHwdK5wH3JkK/9+fGwyRP11VI=
+-----END CERTIFICATE-----
+"""
+
+TEST_CERT_DAYS = [
+    (datetime.datetime(2018, 11, 15, 1, 2, 3), 11),
+    (datetime.datetime(2018, 11, 25, 15, 20, 0), 1),
+    (datetime.datetime(2018, 11, 25, 15, 30, 0), 0),
+]
+
+
+if HAS_CURRENT_CRYPTOGRAPHY:
+    @pytest.mark.parametrize("now, expected_days", TEST_CERT_DAYS)
+    def test_certdays_cryptography(now, expected_days, tmpdir):
+        fn = tmpdir / 'test-cert.pem'
+        fn.write(TEST_CERT)
+        module = MagicMock()
+        days = cryptography_get_cert_days(module, str(fn), now=now)
+        assert days == expected_days