diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+`tasty-hslua` uses [PVP Versioning][1].
+
+[1]: https://pvp.haskell.org
+
+## tasty-hslua 1.0.0
+
+Release pending.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright © 2017-2021 Albert Krewinkel
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# tasty-hslua
+
+[![Build status][GitHub Actions badge]][GitHub Actions]
+[![AppVeyor Status]](https://ci.appveyor.com/project/tarleb/hslua-r2y18)
+[![Hackage]](https://hackage.haskell.org/package/tasty-hslua)
+
+Tasty test helpers to check HsLua operations.
+
+[GitHub Actions badge]: https://img.shields.io/github/workflow/status/hslua/hslua/CI.svg?logo=github
+[GitHub Actions]: https://github.com/hslua/hslua/actions
+[AppVeyor Status]: https://ci.appveyor.com/api/projects/status/ldutrilgxhpcau94/branch/main?svg=true
+[Hackage]: https://img.shields.io/hackage/v/tasty-hslua.svg
diff --git a/src/Test/Tasty/HsLua.hs b/src/Test/Tasty/HsLua.hs
new file mode 100644
--- /dev/null
+++ b/src/Test/Tasty/HsLua.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Module      : Test.Tasty.HsLua
+Copyright   : © 2017-2021 Albert Krewinkel
+License     : MIT
+Maintainer  : Albert Krewinkel <tarleb+hslua@zeitkraut.de>
+Stability   : beta
+Portability : non-portable (depends on GHC)
+
+Utilities for testing of HsLua operations.
+-}
+module Test.Tasty.HsLua
+  ( assertLuaBool
+  , pushLuaExpr
+  , shouldBeErrorMessageOf
+  , shouldBeResultOf
+  , shouldHoldForResultOf
+  , (=:)
+  , (?:)
+  ) where
+
+import Data.ByteString (ByteString, append)
+import HsLua.Core
+  (Lua, LuaE, LuaError, run, runEither, loadstring, call, multret)
+import Test.Tasty (TestTree)
+import Test.Tasty.HUnit
+  (Assertion, HasCallStack, assertBool, assertFailure, testCase, (@?=))
+
+import qualified HsLua.Core as Lua
+
+-- | Takes a Lua expression as a 'ByteString', evaluates it and pushes
+-- the result to the stack.
+--
+-- > -- will return "12"
+-- > run $ do
+-- >   pushLuaExpr "7 + 5"
+-- >   tointeger top
+pushLuaExpr :: LuaError e => ByteString -> LuaE e ()
+pushLuaExpr expr = loadstring ("return " `append` expr) *> call 0 multret
+
+-- | Takes a value and a 'Lua' operation and turns them into an
+-- 'Assertion' which checks that the operation produces the given value.
+shouldBeResultOf :: (HasCallStack, Eq a, Show a)
+                 => a -> Lua a -> Assertion
+shouldBeResultOf expected luaOp = do
+  errOrRes <- runEither luaOp
+  case errOrRes of
+    Left (Lua.Exception msg) -> assertFailure $ "Lua operation failed with "
+                                ++ "message: '" ++ msg ++ "'"
+    Right res -> res @?= expected
+
+-- | Checks whether a 'Lua' operation fails with the given string as
+-- error message.
+shouldBeErrorMessageOf :: (HasCallStack, Show a)
+                       => String -> Lua a -> Assertion
+shouldBeErrorMessageOf expectedErrMsg luaOp = do
+  errOrRes <- runEither luaOp
+  case errOrRes of
+    Left (Lua.Exception msg) -> msg @?= expectedErrMsg
+    Right res ->
+      assertFailure ("Lua operation succeeded unexpectedly and returned "
+                     ++ show res)
+
+-- | Checks whether the return value of an operation holds for the given
+-- predicate.
+shouldHoldForResultOf :: (HasCallStack, Show a)
+                      => (a -> Bool) -> Lua a -> Assertion
+shouldHoldForResultOf predicate luaOp = do
+  errOrRes <- runEither luaOp
+  case errOrRes of
+    Left (Lua.Exception msg) -> assertFailure $ "Lua operation failed with "
+                                ++ "message: '" ++ msg ++ "'"
+    Right res -> assertBool ("predicate doesn't hold for " ++ show res)
+                            (predicate res)
+
+-- | Checks whether the operation returns 'True'.
+assertLuaBool :: HasCallStack => LuaE e Bool -> Assertion
+assertLuaBool luaOp = assertBool "" =<< run luaOp
+
+-- | Creates a new test case with the given name, checking whether the
+-- operation returns 'True'.
+luaTestBool :: HasCallStack => String -> LuaE e Bool -> TestTree
+luaTestBool msg luaOp = testCase msg $
+  assertBool "Lua operation returned false" =<< run luaOp
+
+-- | Infix alias for 'testCase'.
+(=:) :: String -> Assertion -> TestTree
+(=:) = testCase
+infix  3 =:
+
+-- | Infix alias for 'luaTestBool'.
+(?:) :: HasCallStack => String -> LuaE e Bool -> TestTree
+(?:) = luaTestBool
+infixr 3 ?:
diff --git a/tasty-hslua.cabal b/tasty-hslua.cabal
new file mode 100644
--- /dev/null
+++ b/tasty-hslua.cabal
@@ -0,0 +1,55 @@
+cabal-version:       2.2
+name:                tasty-hslua
+version:             1.0.0
+synopsis:            Tasty helpers to test HsLua.
+description:         Various tasty helpers and utilities to test HsLua
+                     oparations. Built on top of tasty-hunit.
+homepage:            https://hslua.org/
+bug-reports:         https://github.com/hslua/hslua/issues
+license:             MIT
+license-file:        LICENSE
+author:              Albert Krewinkel
+maintainer:          albert+hslua@zeitkraut.de
+copyright:           © 2017-2021 Albert Krewinkel
+category:            Foreign
+build-type:          Simple
+extra-source-files:  README.md
+                   , CHANGELOG.md
+tested-with:         GHC == 8.0.2
+                   , GHC == 8.2.2
+                   , GHC == 8.4.4
+                   , GHC == 8.6.5
+                   , GHC == 8.8.4
+                   , GHC == 8.10.4
+                   , GHC == 9.0.1
+
+source-repository head
+  type:                git
+  location:            https://github.com/hslua/hslua.git
+
+common common-options
+  default-language:    Haskell2010
+  build-depends:       base          >= 4.8    && < 5
+                     , hslua-core    >= 2.0    && < 2.1
+                     , bytestring    >= 0.10.2 && < 0.12
+  ghc-options:         -Wall
+                       -Wincomplete-record-updates
+                       -Wnoncanonical-monad-instances
+                       -Wredundant-constraints
+  if impl(ghc >= 8.2)
+    ghc-options:         -Wcpp-undef
+                         -Werror=missing-home-modules
+  if impl(ghc >= 8.4)
+    ghc-options:         -Widentities
+                         -Wincomplete-uni-patterns
+                         -Wpartial-fields
+                         -fhide-source-paths
+
+library
+  import:              common-options
+  exposed-modules:     Test.Tasty.HsLua
+  hs-source-dirs:      src
+  default-extensions:  LambdaCase
+  other-extensions:    OverloadedStrings
+  build-depends:       tasty                >= 0.11
+                     , tasty-hunit          >= 0.9
