diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
--- a/CHANGELOG.markdown
+++ b/CHANGELOG.markdown
@@ -1,3 +1,41 @@
+1.23.3 [2026.01.10]
+-------------------
+* Remove unused `ghc-prim`, `tagged, `transformers-compat`, and `void`
+  dependencies.
+
+1.23.2 [2025.06.17]
+-------------------
+* Replace `test-framework` with `tasty` in the test suite.
+
+1.23.1 [2025.03.03]
+-------------------
+* Add `Uniform` and `UniformRange` instances for `Plucker`, `Quaternion`, `V`,
+  and `V{0,1,2,3,4}`.
+
+1.23 [2024.04.15]
+-----------------
+* The direction of interpolation of `lerp` has been reversed;
+  now `lerp 0 a b == a` and `lerp 1 a b == b`.
+  This brings `lerp` in line not only with its implementation
+  in other languages and frameworks, but also with `slerp` in this package.
+
+1.22 [2022.11.30]
+-----------------
+* The types of `_Point` and `lensP` have been generalized:
+
+  ```diff
+  -_Point :: Iso' (Point f a) (f a)
+  +_Point :: Iso (Point f a) (Point g b) (f a) (g b)
+
+  -lensP :: Lens' (Point g a) (g a)
+  +lensP :: Lens (Point f a) (Point g b) (f a) (g b)
+  ```
+
+  There is a chance that existing uses of `_Point` or `lensP` will fail to
+  typecheck due to their more general types. You can use `_Point.simple` or
+  `lensP.simple` to restore their old, more restricted types (where `simple`
+  comes from `Control.Lens` in the `lens` library).
+
 1.21.10 [2022.06.21]
 --------------------
 * Allow building with `vector-0.13.*`.
diff --git a/linear.cabal b/linear.cabal
--- a/linear.cabal
+++ b/linear.cabal
@@ -1,6 +1,6 @@
 name:          linear
 category:      Math, Algebra
-version:       1.21.10
+version:       1.23.3
 license:       BSD3
 cabal-version: >= 1.10
 license-file:  LICENSE
@@ -20,7 +20,13 @@
              , GHC == 8.8.4
              , GHC == 8.10.7
              , GHC == 9.0.2
-             , GHC == 9.2.2
+             , GHC == 9.2.8
+             , GHC == 9.4.8
+             , GHC == 9.6.7
+             , GHC == 9.8.4
+             , GHC == 9.10.3
+             , GHC == 9.12.2
+             , GHC == 9.14.1
 extra-source-files:
   .gitignore
   .hlint.yaml
@@ -53,23 +59,18 @@
     binary               >= 0.5   && < 0.9,
     bytes                >= 0.15  && < 1,
     cereal               >= 0.4.1.1 && < 0.6,
-    containers           >= 0.4   && < 0.7,
-    deepseq              >= 1.1   && < 1.5,
+    containers           >= 0.4   && < 0.9,
+    deepseq              >= 1.1   && < 1.6,
     distributive         >= 0.5.1 && < 1,
-    ghc-prim,
-    hashable             >= 1.2.7.0 && < 1.5,
+    hashable             >= 1.2.7.0 && < 1.6,
     indexed-traversable  >= 0.1.1 && < 0.2,
     lens                 >= 4.15.2 && < 6,
-    random               >= 1.0   && < 1.3,
+    random               >= 1.2   && < 1.4,
     reflection           >= 2     && < 3,
-    semigroups           >= 0.9   && < 1,
-    semigroupoids        >= 5.2.1 && < 6,
-    tagged               >= 0.8.6 && < 1,
+    semigroupoids        >= 5.2.1 && < 7,
     transformers         >= 0.5   && < 0.7,
-    transformers-compat  >= 0.5.0.4 && < 1,
     unordered-containers >= 0.2.3 && < 0.3,
-    vector               >= 0.12.1.2 && < 0.14,
-    void                 >= 0.6   && < 1
+    vector               >= 0.12.1.2 && < 0.14
 
   if flag(template-haskell) && impl(ghc)
     build-depends: template-haskell >= 2.11.1.0 && < 3.0
@@ -120,12 +121,16 @@
   hs-source-dirs:    tests
   default-language:  Haskell2010
 
-  build-depends: base, simple-reflect >= 0.3.1
+  build-depends: base < 5, simple-reflect >= 0.3.1
 
-test-suite UnitTests
+test-suite test
   type:           exitcode-stdio-1.0
-  main-is:        UnitTests.hs
-  other-modules:  Plucker, Binary, V
+  main-is:        Test.hs
+  other-modules:  Prop.Quaternion
+                  Prop.V3
+                  Unit.Binary
+                  Unit.Plucker
+                  Unit.V
   ghc-options:    -Wall -threaded
   hs-source-dirs: tests
   build-depends:
@@ -133,10 +138,11 @@
     binary,
     bytestring,
     deepseq,
-    test-framework >= 0.8,
-    test-framework-hunit >= 0.3,
-    HUnit >= 1.2.5,
+    tasty >= 1.4 && < 1.6,
+    tasty-hunit >= 0.10 && < 0.11,
+    tasty-quickcheck >= 0.10 && < 0.12,
     linear,
+    QuickCheck >= 2.5,
     reflection,
     vector
   default-language: Haskell2010
diff --git a/src/Linear/Affine.hs b/src/Linear/Affine.hs
--- a/src/Linear/Affine.hs
+++ b/src/Linear/Affine.hs
@@ -14,10 +14,6 @@
 {-# LANGUAGE StandaloneDeriving #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- License     :  BSD-style (see the file LICENSE)
@@ -177,11 +173,11 @@
   liftHashWithSalt h s (P f) = liftHashWithSalt h s f
   {-# INLINE liftHashWithSalt #-}
 
-lensP :: Lens' (Point g a) (g a)
+lensP :: Lens (Point f a) (Point g b) (f a) (g b)
 lensP afb (P a) = P <$> afb a
 {-# INLINE lensP #-}
 
-_Point :: Iso' (Point f a) (f a)
+_Point :: Iso (Point f a) (Point g b) (f a) (g b)
 _Point = iso (\(P a) -> a) P
 {-# INLINE _Point #-}
 
diff --git a/src/Linear/Plucker.hs b/src/Linear/Plucker.hs
--- a/src/Linear/Plucker.hs
+++ b/src/Linear/Plucker.hs
@@ -9,13 +9,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -53,7 +46,9 @@
   , e01, e02, e03, e12, e31, e23
   ) where
 
+#if !MIN_VERSION_base(4,18,0)
 import Control.Applicative
+#endif
 import Control.DeepSeq (NFData(rnf))
 import Control.Monad (liftM)
 import Control.Monad.Fix
@@ -91,7 +86,8 @@
 import Linear.V3
 import Linear.V4
 import Linear.Vector
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 
 -- | Plücker coordinates for lines in a 3-dimensional space.
 data Plucker a = Plucker !a !a !a !a !a !a deriving (Eq,Ord,Show,Read
@@ -121,6 +117,17 @@
           (d'', g4) -> case randomR (e,e') g4 of
             (e'', g5) -> case randomR (f,f') g5 of
               (f'', g6) -> (Plucker a'' b'' c'' d'' e'' f'', g6)
+
+instance Uniform a => Uniform (Plucker a) where
+
+instance UniformRange a => UniformRange (Plucker a) where
+  uniformRM (Plucker a b c d e f, Plucker a' b' c' d' e' f') g = Plucker
+    <$> uniformRM (a, a') g
+    <*> uniformRM (b, b') g
+    <*> uniformRM (c, c') g
+    <*> uniformRM (d, d') g
+    <*> uniformRM (e, e') g
+    <*> uniformRM (f, f') g
 
 instance Functor Plucker where
   fmap g (Plucker a b c d e f) = Plucker (g a) (g b) (g c) (g d) (g e) (g f)
diff --git a/src/Linear/Quaternion.hs b/src/Linear/Quaternion.hs
--- a/src/Linear/Quaternion.hs
+++ b/src/Linear/Quaternion.hs
@@ -10,18 +10,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_base
-#define MIN_VERSION_base(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -96,7 +84,8 @@
 import Linear.V4
 import Linear.Vector
 import Prelude hiding (any)
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 
 -- | Quaternions
 data Quaternion a = Quaternion !a {-# UNPACK #-}!(V3 a)
@@ -119,6 +108,13 @@
   randomR (Quaternion a b, Quaternion c d) g = case randomR (a,c) g of
     (e, g') -> case randomR (b,d) g' of
       (f, g'') -> (Quaternion e f, g'')
+
+instance Uniform a => Uniform (Quaternion a) where
+
+instance UniformRange a => UniformRange (Quaternion a) where
+  uniformRM (Quaternion a b, Quaternion c d) g = Quaternion
+    <$> uniformRM (a, c) g
+    <*> uniformRM (b, d) g
 
 instance Functor Quaternion where
   fmap f (Quaternion e v) = Quaternion (f e) (fmap f v)
diff --git a/src/Linear/V.hs b/src/Linear/V.hs
--- a/src/Linear/V.hs
+++ b/src/Linear/V.hs
@@ -15,22 +15,6 @@
 {-# LANGUAGE Trustworthy #-}
 {-# LANGUAGE DeriveGeneric #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_reflection
-#define MIN_VERSION_reflection(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_base
-#define MIN_VERSION_base(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -104,6 +88,7 @@
 import Data.Semigroup
 #endif
 import System.Random (Random(..))
+import System.Random.Stateful (Uniform(..), UniformRange(..))
 
 class Dim n where
   reflectDim :: p n -> Int
@@ -148,6 +133,15 @@
 instance (Dim n, Random a) => Random (V n a) where
   random = runState (V <$> V.replicateM (reflectDim (Proxy :: Proxy n)) (state random))
   randomR (V ls,V hs) = runState (V <$> V.zipWithM (\l h -> state $ randomR (l,h)) ls hs)
+
+instance (Dim n, Uniform a) => Uniform (V n a) where
+  uniformM g = V <$> V.replicateM (reflectDim (Proxy :: Proxy n)) (uniformM g)
+
+instance (Dim n, UniformRange a) => UniformRange (V n a) where
+  uniformRM (V ls, V hs) g = V <$> V.zipWithM (\l h -> uniformRM (l, h) g) ls hs
+#if (MIN_VERSION_random(1,3,0))
+  isInRange (V ls, V hs) (V xs) = V.and $ V.zipWith3 (\l h x -> isInRange (l, h) x) ls hs xs
+#endif
 
 data ReifiedDim (s :: Type)
 
diff --git a/src/Linear/V0.hs b/src/Linear/V0.hs
--- a/src/Linear/V0.hs
+++ b/src/Linear/V0.hs
@@ -9,22 +9,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_base
-#define MIN_VERSION_base(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -76,7 +60,8 @@
 import Linear.Epsilon
 import Linear.Vector
 import Linear.V
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 import Prelude hiding (sum)
 
 -- $setup
@@ -110,6 +95,11 @@
   randomR _ g = (V0, g)
   randomRs _ _ = repeat V0
   randoms _ = repeat V0
+
+instance Uniform (V0 a) where
+
+instance UniformRange (V0 a) where
+  uniformRM (_, _) _ = pure V0
 
 instance Serial1 V0 where
   serializeWith _ = serialize
diff --git a/src/Linear/V1.hs b/src/Linear/V1.hs
--- a/src/Linear/V1.hs
+++ b/src/Linear/V1.hs
@@ -13,22 +13,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_base
-#define MIN_VERSION_base(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -79,7 +63,8 @@
 import Linear.Epsilon
 import Linear.Vector
 import Prelude hiding (sum)
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 #if !(MIN_VERSION_base(4,11,0))
 import Data.Semigroup
 #endif
@@ -386,6 +371,11 @@
   randoms g = V1 <$> randoms g
   randomR (V1 a, V1 b) g = case randomR (a, b) g of (a', g') -> (V1 a', g')
   randomRs (V1 a, V1 b) g = V1 <$> randomRs (a, b) g
+
+instance Uniform a => Uniform (V1 a) where
+
+instance UniformRange a => UniformRange (V1 a) where
+  uniformRM (V1 a, V1 b) g = V1 <$> uniformRM (a, b) g
 
 instance Eq1 V1 where
   liftEq f (V1 a) (V1 b) = f a b
diff --git a/src/Linear/V2.hs b/src/Linear/V2.hs
--- a/src/Linear/V2.hs
+++ b/src/Linear/V2.hs
@@ -10,22 +10,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_base
-#define MIN_VERSION_base(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -88,7 +72,8 @@
 import Linear.Vector
 import Linear.V1 (R1(..),ex)
 import Prelude hiding (sum)
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 
 -- $setup
 -- >>> import Control.Applicative
@@ -132,6 +117,11 @@
     (x, g') -> case randomR (b, d) g' of
       (y, g'') -> (V2 x y, g'')
   {-# inline randomR #-}
+
+instance Uniform a => Uniform (V2 a) where
+
+instance UniformRange a => UniformRange (V2 a) where
+  uniformRM (V2 a b, V2 c d) g = V2 <$> uniformRM (a, c) g <*> uniformRM (b, d) g
 
 instance Functor V2 where
   fmap f (V2 a b) = V2 (f a) (f b)
diff --git a/src/Linear/V3.hs b/src/Linear/V3.hs
--- a/src/Linear/V3.hs
+++ b/src/Linear/V3.hs
@@ -10,18 +10,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
-
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -45,7 +33,9 @@
   , ex, ey, ez
   ) where
 
+#if !MIN_VERSION_base(4,18,0)
 import Control.Applicative
+#endif
 import Control.DeepSeq (NFData(rnf))
 import Control.Monad (liftM)
 import Control.Monad.Fix
@@ -85,7 +75,8 @@
 import Linear.V
 import Linear.V2
 import Linear.Vector
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 
 -- $setup
 -- >>> import Control.Lens hiding (index)
@@ -128,6 +119,14 @@
     (a'', g') -> case randomR (b,b') g' of
       (b'', g'') -> case randomR (c,c') g'' of
         (c'', g''') -> (V3 a'' b'' c'', g''')
+
+instance Uniform a => Uniform (V3 a) where
+
+instance UniformRange a => UniformRange (V3 a) where
+  uniformRM (V3 a b c, V3 a' b' c') g = V3
+    <$> uniformRM (a, a') g
+    <*> uniformRM (b, b') g
+    <*> uniformRM (c, c') g
 
 instance Traversable V3 where
   traverse f (V3 a b c) = V3 <$> f a <*> f b <*> f c
diff --git a/src/Linear/V4.hs b/src/Linear/V4.hs
--- a/src/Linear/V4.hs
+++ b/src/Linear/V4.hs
@@ -10,17 +10,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE DeriveLift #-}
 
-#ifndef MIN_VERSION_hashable
-#define MIN_VERSION_hashable(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_vector
-#define MIN_VERSION_vector(x,y,z) 1
-#endif
-
-#ifndef MIN_VERSION_transformers
-#define MIN_VERSION_transformers(x,y,z) 1
-#endif
 -----------------------------------------------------------------------------
 -- |
 -- Copyright   :  (C) 2012-2015 Edward Kmett
@@ -51,7 +40,9 @@
   , ex, ey, ez, ew
   ) where
 
+#if !MIN_VERSION_base(4,18,0)
 import Control.Applicative
+#endif
 import Control.DeepSeq (NFData(rnf))
 import Control.Monad (liftM)
 import Control.Monad.Fix
@@ -92,7 +83,8 @@
 import Linear.V2
 import Linear.V3
 import Linear.Vector
-import System.Random (Random(..))
+import System.Random (Random(..), Uniform)
+import System.Random.Stateful (UniformRange(..))
 
 -- $setup
 -- >>> import Control.Lens hiding (index)
@@ -137,6 +129,15 @@
       (b'', g'') -> case randomR (c,c') g'' of
         (c'', g''') -> case randomR (d,d') g''' of
           (d'', g'''') -> (V4 a'' b'' c'' d'', g'''')
+
+instance Uniform a => Uniform (V4 a) where
+
+instance UniformRange a => UniformRange (V4 a) where
+  uniformRM (V4 a b c d, V4 a' b' c' d') g = V4
+    <$> uniformRM (a, a') g
+    <*> uniformRM (b, b') g
+    <*> uniformRM (c, c') g
+    <*> uniformRM (d, d') g
 
 instance Traversable V4 where
   traverse f (V4 a b c d) = V4 <$> f a <*> f b <*> f c <*> f d
diff --git a/src/Linear/Vector.hs b/src/Linear/Vector.hs
--- a/src/Linear/Vector.hs
+++ b/src/Linear/Vector.hs
@@ -133,8 +133,13 @@
   x ^-^ y = x ^+^ negated y
 
   -- | Linearly interpolate between two vectors.
+  --
+  -- /Since linear version 1.23, interpolation direction has been reversed; now/
+  --
+  -- > lerp 0 a b == a
+  -- > lerp 1 a b == b
   lerp :: Num a => a -> f a -> f a -> f a
-  lerp alpha u v = alpha *^ u ^+^ (1 - alpha) *^ v
+  lerp alpha u v = (1 - alpha) *^ u ^+^ alpha *^ v
   {-# INLINE lerp #-}
 
   -- | Apply a function to merge the 'non-zero' components of two vectors, unioning the rest of the values.
diff --git a/tests/Binary.hs b/tests/Binary.hs
deleted file mode 100644
--- a/tests/Binary.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Binary (tests) where
-
-import Data.Binary.Put
-import Data.Binary.Get
-import Linear
-import qualified Data.ByteString.Lazy as BS
-import Test.HUnit
-
-originalVecs :: (V3 Float, V2 Char)
-originalVecs = (V3 1 2 3, V2 'a' 'b')
-
-bytes :: BS.ByteString
-bytes = runPut $ do putLinear $ fst originalVecs
-                    putLinear $ snd originalVecs
-
-tests :: Test
-tests = test [ "Serialized length" ~: BS.length bytes ~?= 3*13+2
-             , "Deserialization" ~: deserialized ~?= originalVecs ]
-  where deserialized = runGet ((,) <$> getLinear <*> getLinear) bytes
diff --git a/tests/Plucker.hs b/tests/Plucker.hs
deleted file mode 100644
--- a/tests/Plucker.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Plucker (tests) where
-import Linear
-import Linear.Plucker
-import Linear.Plucker.Coincides
-import Test.HUnit
-
-ln2,ln3,ln4,ln5,ln6,ln7,ln8,ln9 :: Plucker Float
-ln2 = plucker3D (V3 1 3 0) (V3 1 3 (-2))    -- starting line
-ln3 = plucker3D (V3 2 3 0) (V3 2 3 (-2))    -- parallel
-ln4 = plucker3D (V3 2 4 0) (V3 1 4 (-2))    -- ccw
-ln5 = plucker3D (V3 (-2) 4 0) (V3 2 4 (-2)) -- cw
-ln6 = plucker3D (V3 2 3 0) (V3 1 3 (-2))    -- intersect
-ln7 = plucker3D (V3 1 3 0) (V3 1 3 2)       -- reversed
-ln8 = plucker3D (V3 0 4 4) (V3 0 (-4) (-4)) -- through origin
-ln9 = Plucker 1 2 3 4 5 6                   -- not a 3D line
-
-tests :: Test
-tests = test [ "parallel" ~: parallel ln2 ln3 ~?= True
-             , "CCW" ~: passes ln2 ln4 ~?= Counterclockwise 
-             , "CW" ~: passes ln2 ln5 ~?= Clockwise
-             , "intersect1" ~: intersects ln2 ln6 ~?= True 
-             , "intersect2" ~: intersects ln2 ln3 ~?= False
-             , "line equality 1" ~: Line ln2 == Line ln2 ~?= True 
-             , "line equality 2" ~: Line ln2 == Line ln7 ~?= True 
-             , "line equality 3" ~: Line ln2 == Ray ln7 ~?= True
-             , "line equality 4" ~: Ray ln2 == Line ln7 ~?= True
-             , "ray equality 1" ~: Ray ln2 == Ray ln7 ~?= False
-             , "ray equality 2" ~: Ray ln2 == Ray (3 *^ ln2) ~?= True
-             , "ray equality 3" ~: Ray ln2 == Ray (negate ln7) ~?= True
-             , "quadrance" ~: nearZero (quadranceToOrigin ln2 - 10) ~?= True
-             , "closest 1" ~: 
-                 nearZero (qd (V3 1 3 0) $ closestToOrigin ln2) ~?= True
-             , "closest 2" ~: nearZero (qd 0 $ closestToOrigin ln8) ~?= True
-             , "isLine 1" ~: isLine ln2 ~?= True
-             , "isLine 2" ~: isLine ln9 ~?= False ]
diff --git a/tests/Prop/Quaternion.hs b/tests/Prop/Quaternion.hs
new file mode 100644
--- /dev/null
+++ b/tests/Prop/Quaternion.hs
@@ -0,0 +1,28 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+module Prop.Quaternion (tests) where
+
+import Linear.Quaternion (Quaternion(..))
+import Linear.Epsilon (nearZero)
+import Linear.Vector (lerp)
+import Test.QuickCheck (Arbitrary(..))
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.QuickCheck (testProperty)
+
+import Prop.V3 ()
+
+instance Arbitrary a => Arbitrary (Quaternion a) where
+  arbitrary = Quaternion <$> arbitrary <*> arbitrary
+
+prop_lerp0 :: Quaternion Double -> Quaternion Double -> Bool
+prop_lerp0 a b = nearZero (lerp 0 a b - a)
+
+prop_lerp1 :: Quaternion Double -> Quaternion Double -> Bool
+prop_lerp1 a b = nearZero (lerp 1 a b - b)
+
+tests :: [TestTree]
+tests =
+  [ testGroup "lerp"
+    [ testProperty "lerp 0 a b == a" prop_lerp0
+    , testProperty "lerp 1 a b == b" prop_lerp1
+    ]
+  ]
diff --git a/tests/Prop/V3.hs b/tests/Prop/V3.hs
new file mode 100644
--- /dev/null
+++ b/tests/Prop/V3.hs
@@ -0,0 +1,8 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+module Prop.V3 () where
+
+import Linear.V3 (V3(..))
+import Test.QuickCheck (Arbitrary(..))
+
+instance Arbitrary a => Arbitrary (V3 a) where
+  arbitrary = V3 <$> arbitrary <*> arbitrary <*> arbitrary
diff --git a/tests/Test.hs b/tests/Test.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE CPP #-}
+module Main (main) where
+
+import Test.Tasty (defaultMain, testGroup, TestTree)
+
+import qualified Prop.Quaternion
+import qualified Unit.Binary
+import qualified Unit.Plucker
+import qualified Unit.V
+
+tests :: [TestTree]
+tests =
+  [ testGroup "Property tests"
+    [ testGroup "Quaternion" Prop.Quaternion.tests
+    ]
+  , testGroup "Unit tests"
+    [ testGroup "Binary" Unit.Binary.tests
+    , testGroup "Plucker" Unit.Plucker.tests
+    , testGroup "V" Unit.V.tests
+    ]
+  ]
+
+main :: IO ()
+main = defaultMain $ testGroup "linear" tests
diff --git a/tests/Unit/Binary.hs b/tests/Unit/Binary.hs
new file mode 100644
--- /dev/null
+++ b/tests/Unit/Binary.hs
@@ -0,0 +1,20 @@
+module Unit.Binary (tests) where
+
+import Data.Binary.Put
+import Data.Binary.Get
+import Linear
+import qualified Data.ByteString.Lazy as BS
+import Test.Tasty (TestTree)
+import Test.Tasty.HUnit ((@?=), testCase)
+
+originalVecs :: (V3 Float, V2 Char)
+originalVecs = (V3 1 2 3, V2 'a' 'b')
+
+bytes :: BS.ByteString
+bytes = runPut $ do putLinear $ fst originalVecs
+                    putLinear $ snd originalVecs
+
+tests :: [TestTree]
+tests = [ testCase "Serialized length" $ BS.length bytes @?= 3*13+2
+        , testCase "Deserialization" $ deserialized @?= originalVecs ]
+  where deserialized = runGet ((,) <$> getLinear <*> getLinear) bytes
diff --git a/tests/Unit/Plucker.hs b/tests/Unit/Plucker.hs
new file mode 100644
--- /dev/null
+++ b/tests/Unit/Plucker.hs
@@ -0,0 +1,36 @@
+module Unit.Plucker (tests) where
+import Linear
+import Linear.Plucker
+import Linear.Plucker.Coincides
+import Test.Tasty (TestTree)
+import Test.Tasty.HUnit ((@?=), testCase)
+
+ln2,ln3,ln4,ln5,ln6,ln7,ln8,ln9 :: Plucker Float
+ln2 = plucker3D (V3 1 3 0) (V3 1 3 (-2))    -- starting line
+ln3 = plucker3D (V3 2 3 0) (V3 2 3 (-2))    -- parallel
+ln4 = plucker3D (V3 2 4 0) (V3 1 4 (-2))    -- ccw
+ln5 = plucker3D (V3 (-2) 4 0) (V3 2 4 (-2)) -- cw
+ln6 = plucker3D (V3 2 3 0) (V3 1 3 (-2))    -- intersect
+ln7 = plucker3D (V3 1 3 0) (V3 1 3 2)       -- reversed
+ln8 = plucker3D (V3 0 4 4) (V3 0 (-4) (-4)) -- through origin
+ln9 = Plucker 1 2 3 4 5 6                   -- not a 3D line
+
+tests :: [TestTree]
+tests = [ testCase "parallel" $ parallel ln2 ln3 @?= True
+        , testCase "CCW" $ passes ln2 ln4 @?= Counterclockwise
+        , testCase "CW" $ passes ln2 ln5 @?= Clockwise
+        , testCase "intersect1" $ intersects ln2 ln6 @?= True
+        , testCase "intersect2" $ intersects ln2 ln3 @?= False
+        , testCase "line equality 1" $ Line ln2 == Line ln2 @?= True
+        , testCase "line equality 2" $ Line ln2 == Line ln7 @?= True
+        , testCase "line equality 3" $ Line ln2 == Ray ln7 @?= True
+        , testCase "line equality 4" $ Ray ln2 == Line ln7 @?= True
+        , testCase "ray equality 1" $ Ray ln2 == Ray ln7 @?= False
+        , testCase "ray equality 2" $ Ray ln2 == Ray (3 *^ ln2) @?= True
+        , testCase "ray equality 3" $ Ray ln2 == Ray (negate ln7) @?= True
+        , testCase "quadrance" $ nearZero (quadranceToOrigin ln2 - 10) @?= True
+        , testCase "closest 1" $
+            nearZero (qd (V3 1 3 0) $ closestToOrigin ln2) @?= True
+        , testCase "closest 2" $ nearZero (qd 0 $ closestToOrigin ln8) @?= True
+        , testCase "isLine 1" $ isLine ln2 @?= True
+        , testCase "isLine 2" $ isLine ln9 @?= False ]
diff --git a/tests/Unit/V.hs b/tests/Unit/V.hs
new file mode 100644
--- /dev/null
+++ b/tests/Unit/V.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE DataKinds #-}
+module Unit.V (tests) where
+
+import Control.DeepSeq (rnf)
+import qualified Data.Vector.Unboxed as U (fromList)
+import Linear.V (V)
+import Test.Tasty (TestTree)
+import Test.Tasty.HUnit ((@?=), testCase)
+
+v10 :: V 10 Int
+v10 = return 5
+
+tests :: [TestTree]
+tests = [ testCase "GH124" $ rnf (U.fromList [v10]) @?= () ]
diff --git a/tests/UnitTests.hs b/tests/UnitTests.hs
deleted file mode 100644
--- a/tests/UnitTests.hs
+++ /dev/null
@@ -1,16 +0,0 @@
-{-# LANGUAGE CPP #-}
-module Main (main) where
-import Test.Framework (defaultMain, testGroup, Test)
-import Test.Framework.Providers.HUnit
-import qualified Plucker
-import qualified Binary
-import qualified V
-
-tests :: [Test]
-tests = [ testGroup "Plucker" $ hUnitTestToTests Plucker.tests
-        , testGroup "Binary" $ hUnitTestToTests Binary.tests
-        , testGroup "V" $ hUnitTestToTests V.tests
-        ]
-
-main :: IO ()
-main = defaultMain tests
diff --git a/tests/V.hs b/tests/V.hs
deleted file mode 100644
--- a/tests/V.hs
+++ /dev/null
@@ -1,13 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-module V (tests) where
-
-import Control.DeepSeq (rnf)
-import qualified Data.Vector.Unboxed as U (fromList)
-import Linear.V (V)
-import Test.HUnit
-
-v10 :: V 10 Int
-v10 = return 5
-
-tests :: Test
-tests = test [ "GH124" ~: rnf (U.fromList [v10]) ~?= () ]
