diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# 0.1.1.0 - August 2021
+
+* Add `replicate`
+* Various optimizations
+* Switch to the [`tasty` framework](https://hackage.haskell.org/package/tasty) for benchmarks and tests
+* Extend the test suite
+  - Add properties for more functions and some typeclass instances
+  - Add strictness tests
+
+# 0.1.0.0 - June 2021
+
+* Initial release
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
 # rbb-vector
 
-An implementation of a Relaxed Radix Balanced Vector (RRB-Vector).
+An implementation of a Relaxed Radix Balanced Vector (RRB-Vector), an efficient sequence data structure.
+It supports fast indexing, iteration, concatenation and splitting.
 
 For more information, see [`rrb-vector` on Hackage](https://hackage.haskell.org/package/rrb-vector).
diff --git a/bench/Main.hs b/bench/Main.hs
--- a/bench/Main.hs
+++ b/bench/Main.hs
@@ -1,22 +1,24 @@
+{-# LANGUAGE BangPatterns #-}
+
 import Data.Functor ((<&>))
 
-import Gauge.Main
+import Test.Tasty.Bench
 
 import qualified Data.RRBVector as RRB
 
 main :: IO ()
-main = defaultMain $ [10, 100, 1_000, 10_000, 100_000] <&> \n ->
-    let v = RRB.fromList [1..n]
-        idx = [n `div` 10, n `div` 2, n - n `div` 10]
+main = defaultMain $ [10, 100, 1000, 10000, 100000] <&> \n ->
+    let !v = RRB.fromList [1..n]
+        !idx = n `div` 2
     in bgroup (show n)
     [ bench "fromList" $ nf RRB.fromList [1..n]
-    , bench "><" $ nf (\vec -> vec RRB.>< vec) v
-    , bench "|>" $ nf (RRB.|> 42) v
-    , bench "<|" $ nf (42 RRB.<|) v
-    , bgroup "take" $ idx <&> \i -> bench (show i) $ nf (RRB.take i) v
-    , bgroup "drop" $ idx <&> \i -> bench (show i) $ nf (RRB.drop i) v
-    , bgroup "lookup" $ idx <&> \i -> bench (show i) $ nf (RRB.lookup i) v
-    , bgroup "adjust" $ idx <&> \i -> bench (show i) $ nf (RRB.adjust i (+ 1)) v
+    , bench "><" $ whnf (\vec -> vec RRB.>< vec) v
+    , bench "|>" $ whnf (RRB.|> 42) v
+    , bench "<|" $ whnf (42 RRB.<|) v
+    , bench "take" $ whnf (RRB.take idx) v
+    , bench "drop" $ whnf (RRB.drop idx) v
+    , bench "index" $ nf (RRB.lookup idx) v
+    , bench "adjust" $ whnf (RRB.adjust idx (+ 1)) v
     , bench "foldl" $ nf (foldl (+) 0) v
     , bench "foldr" $ nf (foldr (+) 0) v
     ]
diff --git a/bench/Traverse.hs b/bench/Traverse.hs
--- a/bench/Traverse.hs
+++ b/bench/Traverse.hs
@@ -1,21 +1,25 @@
 {-# LANGUAGE BangPatterns #-}
 
+import Control.Applicative (liftA2)
+import Control.Monad (replicateM)
 import Data.Foldable (foldl', toList)
 import Data.Functor
 
-import Gauge.Main
+import Test.Tasty.Bench
 
 import qualified Data.RRBVector as RRB
 
 main :: IO ()
-main = defaultMain $ [10, 100, 1_000] <&> \n ->
+main = defaultMain $ [100, 1000, 10000] <&> \n ->
     let !v = RRB.fromList [1..n]
+        !idx = n `div` 2
     in bgroup (show n)
-        [ bench "f1 (where)" $ nf (\v -> f1 v v) v
-        , bench "f2" $ nf (\v -> f2 v v) v
+        [ bench "foldr" $ nf (foldr (+) 0) v
+        , bench "ifoldr" $ nf (RRB.ifoldr (\i x acc -> i + x + acc) 0) v
+        , bench "foldl" $ nf (foldl (+) 0) v
+        , bench "ifoldl" $ nf (RRB.ifoldl (\i acc x -> i + acc + x) 0) v
+        , bench "map" $ nf (RRB.map (+ 1)) v
+        , bench "imap" $ nf (RRB.imap (+)) v
+        , bench "traverse" $ nf (traverse (Just $!)) v
+        , bench "itraverse" $ nf (RRB.itraverse (\i x -> Just $! i + x)) v
         ]
-  where
-    f1 xs ys = foldl' (\acc x -> acc RRB.>< RRB.fromList (replicate n x)) RRB.empty xs
-      where
-        n = length ys
-    f2 xs ys = foldl' (\acc x -> acc RRB.>< RRB.fromList (replicate (length ys) x)) RRB.empty xs
diff --git a/rrb-vector.cabal b/rrb-vector.cabal
--- a/rrb-vector.cabal
+++ b/rrb-vector.cabal
@@ -1,5 +1,5 @@
 name:               rrb-vector
-version:            0.1.0.0
+version:            0.1.1.0
 synopsis:           Efficient RRB-Vectors
 description:
   An RRB-Vector is an efficient sequence data structure.
@@ -18,13 +18,15 @@
 copyright:          2021 konsumlamm
 category:           Data Structures
 build-type:         Simple
-extra-source-files: README.md
+extra-source-files:
+  CHANGELOG.md
+  README.md
 cabal-version:      2.0
 tested-with:        GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.5, GHC == 9.0.1
 
 source-repository head
   type:     git
-  location: https://github.com/konsumlamm/rbb-vector
+  location: https://github.com/konsumlamm/rrb-vector
 
 library
   hs-source-dirs:       src
@@ -43,12 +45,18 @@
 
 test-suite test
   hs-source-dirs:       test
-  main-is:              Spec.hs
+  main-is:              Main.hs
+  other-modules:
+    Arbitrary
+    Properties
+    Strictness
   type:                 exitcode-stdio-1.0
   ghc-options:          -Wall -Wno-orphans -Wno-type-defaults
-  build-depends:        base >= 4.11 && < 5, rrb-vector, hspec, QuickCheck
+  build-depends:        base >= 4.11 && < 5, deepseq, indexed-traversable, rrb-vector, tasty, tasty-quickcheck
+  if impl(ghc == 8.10.*)
+    build-depends:      ghc-heap-view
   default-language:     Haskell2010
-  default-extensions:   ExtendedDefaultRules, NumericUnderscores
+  default-extensions:   ExtendedDefaultRules, ScopedTypeVariables
 
 benchmark rrb-bench
   hs-source-dirs:       bench
@@ -56,8 +64,8 @@
   type:                 exitcode-stdio-1.0
   default-language:     Haskell2010
   ghc-options:          -O2
-  build-depends:        base >= 4.11 && < 5, gauge, rrb-vector
-  default-extensions:   ExtendedDefaultRules, NumericUnderscores
+  build-depends:        base >= 4.11 && < 5, rrb-vector, tasty-bench
+  default-extensions:   ExtendedDefaultRules
 
 benchmark traverse
   hs-source-dirs:       bench
@@ -65,5 +73,5 @@
   type:                 exitcode-stdio-1.0
   default-language:     Haskell2010
   ghc-options:          -O2
-  build-depends:        base >= 4.11 && < 5, gauge, rrb-vector, primitive
-  default-extensions:   ExtendedDefaultRules, NumericUnderscores
+  build-depends:        base >= 4.11 && < 5, primitive, rrb-vector, tasty-bench
+  default-extensions:   ExtendedDefaultRules
diff --git a/src/Data/RRBVector.hs b/src/Data/RRBVector.hs
--- a/src/Data/RRBVector.hs
+++ b/src/Data/RRBVector.hs
@@ -21,7 +21,7 @@
 module Data.RRBVector
     ( Vector
     -- * Construction
-    , empty, singleton, fromList
+    , empty, singleton, fromList, replicate
     -- ** Concatenation
     , (<|), (|>), (><)
     -- * Deconstruction
@@ -45,7 +45,7 @@
     , zip, zipWith, unzip
     ) where
 
-import Prelude hiding (lookup, take, drop, splitAt, map, reverse, zip, zipWith, unzip)
+import Prelude hiding (replicate, lookup, take, drop, splitAt, map, reverse, zip, zipWith, unzip)
 
 import Data.Foldable.WithIndex
 import Data.Functor.WithIndex
diff --git a/src/Data/RRBVector/Internal.hs b/src/Data/RRBVector/Internal.hs
--- a/src/Data/RRBVector/Internal.hs
+++ b/src/Data/RRBVector/Internal.hs
@@ -10,7 +10,7 @@
     -- * Internal
     , blockShift, blockSize, treeSize, computeSizes, up
     -- * Construction
-    , empty, singleton, fromList
+    , empty, singleton, fromList, replicate
     -- ** Concatenation
     , (<|), (|>), (><)
     -- * Deconstruction
@@ -43,12 +43,13 @@
 import Data.Foldable (Foldable(..), for_)
 import Data.Functor.Classes
 import Data.Functor.Identity (Identity(..))
-import Data.Maybe (fromMaybe)
 import qualified Data.List as List
+import Data.Maybe (fromMaybe)
+import Data.Semigroup
 import qualified GHC.Exts as Exts
 import GHC.Stack (HasCallStack)
+import Prelude hiding (replicate, lookup, map, take, drop, splitAt, head, last, reverse, zip, zipWith, unzip)
 import Text.Read
-import Prelude hiding (lookup, map, take, drop, splitAt, head, last, reverse, zip, zipWith, unzip)
 
 import Data.Functor.WithIndex
 import Data.Foldable.WithIndex
@@ -63,13 +64,15 @@
 infixr 5 <|
 infixl 5 |>
 
+type Shift = Int
+
 -- Invariant: Children of a Balanced node are always balanced.
 -- A Leaf node is considered balanced.
 -- Nodes are always non-empty.
 data Tree a
-    = Balanced !(A.Array (Tree a))
-    | Unbalanced !(A.Array (Tree a)) !(PrimArray Int)
-    | Leaf !(A.Array a)
+    = Balanced {-# UNPACK #-} !(A.Array (Tree a))
+    | Unbalanced {-# UNPACK #-} !(A.Array (Tree a)) !(PrimArray Int)
+    | Leaf {-# UNPACK #-} !(A.Array a)
 
 -- | A vector.
 --
@@ -78,35 +81,31 @@
     = Empty
     | Root
         !Int -- size
-        !Int -- shift (blockShift * height)
+        !Shift -- shift (blockShift * height)
         !(Tree a)
 
 -- The number of bits used per level.
-blockShift :: Int
+blockShift :: Shift
 blockShift = 4
-{-# INLINE blockShift #-}
 
 -- The maximum size of a block.
 blockSize :: Int
-blockSize = 1 `shiftL` blockShift
+blockSize = 1 `unsafeShiftL` blockShift
 
 -- The mask used to extract the index into the array.
 blockMask :: Int
 blockMask = blockSize - 1
 
-up :: Int -> Int
+up :: Shift -> Shift
 up sh = sh + blockShift
-{-# INLINE up #-}
 
-down :: Int -> Int
+down :: Shift -> Shift
 down sh = sh - blockShift
-{-# INLINE down #-}
 
-radixIndex :: Int -> Int -> Int
-radixIndex i sh = i `shiftR` sh .&. blockMask
-{-# INLINE radixIndex #-}
+radixIndex :: Int -> Shift -> Int
+radixIndex i sh = i `unsafeShiftR` sh .&. blockMask
 
-relaxedRadixIndex :: PrimArray Int -> Int -> Int -> (Int, Int)
+relaxedRadixIndex :: PrimArray Int -> Int -> Shift -> (Int, Int)
 relaxedRadixIndex sizes i sh =
     let guess = radixIndex i sh -- guess <= idx
         idx = loop guess
@@ -116,7 +115,6 @@
     loop idx =
         let current = indexPrimArray sizes idx -- idx will always be in range for a well-formed tree
         in if i < current then idx else loop (idx + 1)
-{-# INLINE relaxedRadixIndex #-}
 
 treeToArray :: Tree a -> A.Array (Tree a)
 treeToArray (Balanced arr) = arr
@@ -129,44 +127,52 @@
 treeBalanced (Leaf _) = True
 
 -- @treeSize sh@ is the size of a tree with shift @sh@.
-treeSize :: Int -> Tree a -> Int
+treeSize :: Shift -> Tree a -> Int
 treeSize = go 0
   where
-    go acc _ (Leaf arr) = acc + length arr
+    go !acc !_ (Leaf arr) = acc + length arr
     go acc _ (Unbalanced _ sizes) = acc + indexPrimArray sizes (sizeofPrimArray sizes - 1)
     go acc sh (Balanced arr) =
         let i = length arr - 1
-        in go (acc + i * (1 `shiftL` sh)) (down sh) (A.index arr i)
+        in go (acc + i * (1 `unsafeShiftL` sh)) (down sh) (A.index arr i)
 {-# INLINE treeSize #-}
 
 -- @computeSizes sh@ turns an array into a tree node by computing the sizes of its subtrees.
 -- @sh@ is the shift of the resulting tree.
-computeSizes :: Int -> A.Array (Tree a) -> Tree a
-computeSizes sh arr = runST $ do
-    let len = length arr
-        maxSize = 1 `shiftL` sh -- the maximum size of a subtree
-    sizes <- newPrimArray len
-    let loop acc isBalanced i
-            | i < len =
-                let subtree = A.index arr i
-                    size = treeSize (down sh) subtree
-                    acc' = acc + size
-                    isBalanced' = isBalanced && if i == len - 1 then treeBalanced subtree else size == maxSize
-                in writePrimArray sizes i acc' *> loop acc' isBalanced' (i + 1)
-            | otherwise = pure isBalanced
-    isBalanced <- loop 0 True 0
-    if isBalanced then
-        pure $ Balanced arr
-    else do
-        sizes <- unsafeFreezePrimArray sizes -- safe because the mutable @sizes@ isn't used afterwards
-        pure $ Unbalanced arr sizes
+computeSizes :: Shift -> A.Array (Tree a) -> Tree a
+computeSizes !sh arr
+    | isBalanced = Balanced arr
+    | otherwise = runST $ do
+        sizes <- newPrimArray (length arr)
+        let loop acc i
+                | i < len =
+                    let size = treeSize (down sh) (A.index arr i)
+                        acc' = acc + size
+                    in writePrimArray sizes i acc' *> loop acc' (i + 1)
+                | otherwise = do
+                    sizes <- unsafeFreezePrimArray sizes -- safe because the mutable @sizes@ isn't used afterwards
+                    pure $ Unbalanced arr sizes
+        loop 0 0
+  where
+    maxSize = 1 `unsafeShiftL` sh -- the maximum size of a subtree
 
+    len = length arr
+
+    lenM1 = len - 1
+
+    isBalanced = go 0
+      where
+        go i
+            | i < lenM1 = treeSize (down sh) subtree == maxSize && go (i + 1)
+            | otherwise = treeBalanced subtree
+          where
+            subtree = A.index arr i
+
 -- Integer log base 2.
 log2 :: Int -> Int
 log2 x = bitSizeMinus1 - countLeadingZeros x
   where
     bitSizeMinus1 = finiteBitSize (0 :: Int) - 1
-{-# INLINE log2 #-}
 
 instance Show1 Vector where
     liftShowsPrec sp sl p v = showsUnaryWith (liftShowsPrec sp sl) "fromList" p (toList v)
@@ -195,7 +201,8 @@
     compare = compare1
 
 instance Semigroup (Vector a) where
-    v1 <> v2 = v1 >< v2
+    (<>) = (><)
+    stimes = stimesMonoid
 
 instance Monoid (Vector a) where
     mempty = empty
@@ -243,45 +250,47 @@
 
     null Empty = True
     null Root{} = False
-    {-# INLINE null #-}
 
     length Empty = 0
     length (Root s _ _) = s
-    {-# INLINE length #-}
 
 instance FoldableWithIndex Int Vector where
-    ifoldr f z0 v = foldr (\x g !i -> f i x (g (i + 1))) (const z0) v 0
+    ifoldr f z0 v = foldr' (\x g !i -> f i x (g (i + 1))) (const z0) v 0
+    {-# INLINE ifoldr #-}
 
-    ifoldl f z0 v = foldl (\g x !i -> f i (g (i - 1)) x) (const z0) v (length v - 1)
+    ifoldl f z0 v = foldl' (\g x !i -> f i (g (i - 1)) x) (const z0) v (length v - 1)
+    {-# INLINE ifoldl #-}
 
 instance Functor Vector where
     fmap = map
-    x <$ v = fromList (replicate (length v) x)
+    x <$ v = replicate (length v) x
 
 instance FunctorWithIndex Int Vector where
     imap f v = runIdentity $ evalIndexed (traverse (Indexed . f') v) 0
       where
-        f' x i = i `seq` WithIndex (i + 1) (Identity (f i x))
+        f' x !i = WithIndex (i + 1) (Identity (f i x))
 
 instance Traversable Vector where
     traverse _ Empty = pure Empty
     traverse f (Root size sh tree) = Root size sh <$> traverseTree tree
       where
         traverseTree (Balanced arr) = Balanced <$> A.traverse' traverseTree arr
-        traverseTree (Unbalanced arr sizes) = Unbalanced <$> A.traverse' traverseTree arr <*> pure sizes
+        traverseTree (Unbalanced arr sizes) = flip Unbalanced sizes <$> A.traverse' traverseTree arr
         traverseTree (Leaf arr) = Leaf <$> A.traverse f arr
+    {-# INLINE traverse #-}
 
 instance TraversableWithIndex Int Vector where
     itraverse f v = evalIndexed (traverse (Indexed . f') v) 0
       where
-        f' x i = i `seq` WithIndex (i + 1) (f i x)
+        f' x !i = WithIndex (i + 1) (f i x)
+    {-# INLINE itraverse #-}
 
 instance Applicative Vector where
     pure = singleton
     fs <*> xs = foldl' (\acc f -> acc >< map f xs) empty fs
     liftA2 f xs ys = foldl' (\acc x -> acc >< map (f x) ys) empty xs
     xs *> ys = foldl' (\acc _ -> acc >< ys) empty xs
-    xs <* ys = foldl' (\acc x -> acc >< fromList (replicate (length ys) x)) empty xs
+    xs <* ys = foldl' (\acc x -> acc >< replicate (length ys) x) empty xs
 
 instance Monad Vector where
     xs >>= f = foldl' (\acc x -> acc >< f x) empty xs
@@ -343,14 +352,16 @@
         buffer <- Buffer.new blockSize
         let loop [] = do
                 result <- Buffer.get buffer
-                pure [f result]
+                let !x = f result
+                pure [x]
             loop (t : ts) = do
                 size <- Buffer.size buffer
                 if size == blockSize then do
                     result <- Buffer.get buffer
                     Buffer.push buffer t
                     rest <- loop ts
-                    pure (f result : rest)
+                    let !x = f result
+                    pure (x : rest)
                 else do
                     Buffer.push buffer t
                     loop ts
@@ -361,6 +372,27 @@
         [tree] -> Root (treeSize sh tree) sh tree
         trees' -> iterateNodes (up sh) trees'
 
+-- | \(O(\log n)\). @replicate n x@ creates a vector of length @n@ with every element set to @x@.
+--
+-- >>> replicate 5 42
+-- fromList [42,42,42,42,42]
+--
+-- @since 0.1.1.0
+replicate :: Int -> a -> Vector a
+replicate n x
+    | n <= 0 = Empty
+    | n <= blockSize = Root n 0 (Leaf $ A.replicate n x)
+    | otherwise = iterateNodes blockShift (Leaf $ A.replicate blockSize x) (Leaf $ A.replicate (lastIdx .&. blockMask + 1) x)
+  where
+    lastIdx = n - 1
+
+    -- @full@ is a full subtree, @rest@ is the last subtree
+    iterateNodes !sh !full !rest =
+        let subtreesM1 = lastIdx `unsafeShiftR` sh -- the number of subtrees minus 1
+            full' = Balanced $ A.replicate blockSize full
+            rest' = Balanced $ A.replicateSnoc (subtreesM1 .&. blockMask) full rest
+        in if subtreesM1 < blockSize then Root n sh rest' else iterateNodes (up sh) full' rest'
+
 -- | \(O(\log n)\). The element at the index or 'Nothing' if the index is out of range.
 lookup :: Int -> Vector a -> Maybe a
 lookup _ Empty = Nothing
@@ -377,6 +409,7 @@
 -- | \(O(\log n)\). The element at the index. Calls 'error' if the index is out of range.
 index :: HasCallStack => Int -> Vector a -> a
 index i = fromMaybe (error "AMT.index: index out of range") . lookup i
+{-# INLINE index #-}
 
 -- | \(O(\log n)\). A flipped version of 'lookup'.
 (!?) :: Vector a -> Int -> Maybe a
@@ -451,7 +484,7 @@
 --
 -- > zip = zipWith (,)
 zip :: Vector a -> Vector b -> Vector (a, b)
-zip v1 v2 = fromList $ List.zip (toList v1) (toList v2)
+zip = zipWith (,)
 
 -- | \(O(\min(n_1, n_2))\). 'zipWith' generalizes 'zip' by zipping with the function.
 zipWith :: (a -> b -> c) -> Vector a -> Vector b -> Vector c
@@ -462,7 +495,10 @@
 -- >>> unzip (fromList [(1, "a"), (2, "b"), (3, "c")])
 -- (fromList [1,2,3],fromList ["a","b","c"])
 unzip :: Vector (a, b) -> (Vector a, Vector b)
-unzip v = (map fst v, map snd v)
+unzip v =
+    let !left = map fst v
+        !right = map snd v
+    in (left, right)
 
 -- | \(O(\log n)\). The first element and the vector without the first element, or 'Nothing' if the vector is empty.
 --
@@ -516,46 +552,50 @@
 v >< Empty = v
 Root size1 sh1 tree1 >< Root size2 sh2 tree2 =
     let maxShift = max sh1 sh2
-        newTree = mergeTrees tree1 sh1 tree2 sh2
-    in case singleTree newTree of
-        Just newTree -> Root (size1 + size2) maxShift newTree
-        Nothing -> Root (size1 + size2) (up maxShift) newTree
+        upMaxShift = up maxShift
+        newArr = mergeTrees tree1 sh1 tree2 sh2
+    in if length newArr == 1
+        then Root (size1 + size2) maxShift (A.head newArr)
+        else Root (size1 + size2) upMaxShift (computeSizes upMaxShift newArr)
   where
-    mergeTrees (Leaf arr1) _ (Leaf arr2) _ = Balanced $
-        if length arr1 == blockSize then A.from2 (Leaf arr1) (Leaf arr2)
-        else if length arr1 + length arr2 <= blockSize then A.singleton (Leaf (arr1 <> arr2))
-        else
+    mergeTrees tree1@(Leaf arr1) !_ tree2@(Leaf arr2) !_
+        | length arr1 == blockSize = A.from2 tree1 tree2
+        | length arr1 + length arr2 <= blockSize = A.singleton $! Leaf (arr1 <> arr2)
+        | otherwise =
             let (left, right) = A.splitAt (arr1 <> arr2) blockSize
-            in A.from2 (Leaf left) (Leaf right)
+                !leftTree = Leaf left
+                !rightTree = Leaf right
+            in A.from2 leftTree rightTree
     mergeTrees tree1 sh1 tree2 sh2 = case compare sh1 sh2 of
         LT ->
-            let right = treeToArray tree2
-                (rightHead, rightTail) = viewl right
+            let !right = treeToArray tree2
+                (rightHead, rightTail) = viewlArr right
                 merged = mergeTrees tree1 sh1 rightHead (down sh2)
-            in mergeRebalance sh2 A.empty (treeToArray merged) rightTail
+            in mergeRebalance sh2 A.empty merged rightTail
         GT ->
-            let left = treeToArray tree1
-                (leftInit, leftLast) = viewr left
+            let !left = treeToArray tree1
+                (leftInit, leftLast) = viewrArr left
                 merged = mergeTrees leftLast (down sh1) tree2 sh2
-            in mergeRebalance sh1 leftInit (treeToArray merged) A.empty
+            in mergeRebalance sh1 leftInit merged A.empty
         EQ ->
-            let left = treeToArray tree1
-                right = treeToArray tree2
-                (leftInit, leftLast) = viewr left
-                (rightHead, rightTail) = viewl right
+            let !left = treeToArray tree1
+                !right = treeToArray tree2
+                (leftInit, leftLast) = viewrArr left
+                (rightHead, rightTail) = viewlArr right
                 merged = mergeTrees leftLast (down sh1) rightHead (down sh2)
-            in mergeRebalance sh1 leftInit (treeToArray merged) rightTail
+            in mergeRebalance sh1 leftInit merged rightTail
       where
-        viewl arr = (A.head arr, A.drop arr 1)
-        viewr arr = (A.take arr (length arr - 1), A.last arr)
+        viewlArr arr = (A.head arr, A.drop arr 1)
 
-    -- the type annotations are necessary to compile
-    mergeRebalance :: forall a. Int -> A.Array (Tree a) -> A.Array (Tree a) -> A.Array (Tree a) -> Tree a
-    mergeRebalance sh left center right
+        viewrArr arr = (A.take arr (length arr - 1), A.last arr)
+
+    -- the type signature is necessary to compile
+    mergeRebalance :: forall a. Shift -> A.Array (Tree a) -> A.Array (Tree a) -> A.Array (Tree a) -> A.Array (Tree a)
+    mergeRebalance !sh !left !center !right
         | sh == blockShift = mergeRebalance' (\(Leaf arr) -> arr) Leaf
         | otherwise = mergeRebalance' treeToArray (computeSizes (down sh))
       where
-        mergeRebalance' :: (Tree a -> A.Array t) -> (A.Array t -> Tree a) -> Tree a
+        mergeRebalance' :: (Tree a -> A.Array t) -> (A.Array t -> Tree a) -> A.Array (Tree a)
         mergeRebalance' extract construct = runST $ do
             newRoot <- Buffer.new blockSize
             newSubtree <- Buffer.new blockSize
@@ -570,20 +610,14 @@
                     Buffer.push newNode x
             pushTo construct newNode newSubtree
             pushTo (computeSizes sh) newSubtree newRoot
-            computeSizes (up sh) <$> Buffer.get newRoot
+            Buffer.get newRoot
         {-# INLINE mergeRebalance' #-}
 
         pushTo f from to = do
             result <- Buffer.get from
-            Buffer.push to (f result)
+            Buffer.push to $! f result
         {-# INLINE pushTo #-}
 
-    singleTree (Balanced arr)
-        | length arr == 1 = Just (A.head arr)
-    singleTree (Unbalanced arr _)
-        | length arr == 1 = Just (A.head arr)
-    singleTree _ = Nothing
-
 -- | \(O(\log n)\). Add an element to the left end of the vector.
 --
 -- >>> 1 <| fromList [2, 3, 4]
@@ -591,28 +625,28 @@
 (<|) :: a -> Vector a -> Vector a
 x <| Empty = singleton x
 x <| Root size sh tree
-    | insertShift > sh = Root (size + 1) insertShift (computeSizes insertShift (A.from2 (newBranch x sh) tree))
+    | insertShift > sh = Root (size + 1) insertShift (computeSizes insertShift (let !new = newBranch x sh in A.from2 new tree))
     | otherwise = Root (size + 1) sh (consTree sh tree)
   where
     consTree sh (Balanced arr)
-        | sh == insertShift = computeSizes sh (A.cons arr (newBranch x (down sh)))
+        | sh == insertShift = computeSizes sh (A.cons arr $! newBranch x (down sh))
         | otherwise = computeSizes sh (A.adjust' arr 0 (consTree (down sh)))
     consTree sh (Unbalanced arr _)
-        | sh == insertShift = computeSizes sh (A.cons arr (newBranch x (down sh)))
+        | sh == insertShift = computeSizes sh (A.cons arr $! newBranch x (down sh))
         | otherwise = computeSizes sh (A.adjust' arr 0 (consTree (down sh)))
     consTree _ (Leaf arr) = Leaf $ A.cons arr x
 
     insertShift = computeShift size sh (up sh) tree
 
     -- compute the shift at which the new branch needs to be inserted (0 means there is space in the leaf)
-    -- the index is computed for efficient calculation of the shift in a balanced subtree
-    computeShift i sh min (Balanced _) =
-        let newShift = (log2 i `div` blockShift) * blockShift
+    -- the size is computed for efficient calculation of the shift in a balanced subtree
+    computeShift !sz !sh !min (Balanced _) =
+        let newShift = (log2 sz `div` blockShift) * blockShift
         in if newShift > sh then min else newShift
     computeShift _ sh min (Unbalanced arr sizes) =
-        let i' = indexPrimArray sizes 0 -- the size of the first subtree
+        let sz' = indexPrimArray sizes 0 -- the size of the first subtree
             newMin = if length arr < blockSize then sh else min
-        in computeShift i' (down sh) newMin (A.head arr)
+        in computeShift sz' (down sh) newMin (A.head arr)
     computeShift _ _ min (Leaf arr) = if length arr < blockSize then 0 else min
 
 -- | \(O(\log n)\). Add an element to the right end of the vector.
@@ -622,14 +656,14 @@
 (|>) :: Vector a -> a -> Vector a
 Empty |> x = singleton x
 Root size sh tree |> x
-    | insertShift > sh = Root (size + 1) insertShift (computeSizes insertShift (A.from2 tree (newBranch x sh)))
+    | insertShift > sh = Root (size + 1) insertShift (computeSizes insertShift (A.from2 tree $! newBranch x sh))
     | otherwise = Root (size + 1) sh (snocTree sh tree)
   where
     snocTree sh (Balanced arr)
-        | sh == insertShift = Balanced $ A.snoc arr (newBranch x (down sh)) -- the current subtree is fully balanced
+        | sh == insertShift = Balanced (A.snoc arr $! newBranch x (down sh)) -- the current subtree is fully balanced
         | otherwise = Balanced $ A.adjust' arr (length arr - 1) (snocTree (down sh))
     snocTree sh (Unbalanced arr sizes)
-        | sh == insertShift = Unbalanced (A.snoc arr (newBranch x (down sh))) newSizesSnoc
+        | sh == insertShift = Unbalanced (A.snoc arr $! newBranch x (down sh)) newSizesSnoc
         | otherwise = Unbalanced (A.adjust' arr (length arr - 1) (snocTree (down sh))) newSizesAdjust
       where
         -- snoc the last size + 1
@@ -653,23 +687,23 @@
     insertShift = computeShift size sh (up sh) tree
 
     -- compute the shift at which the new branch needs to be inserted (0 means there is space in the leaf)
-    -- the index is computed for efficient calculation of the shift in a balanced subtree
-    computeShift i sh min (Balanced _) =
-        let newShift = (countTrailingZeros i `div` blockShift) * blockShift
+    -- the size is computed for efficient calculation of the shift in a balanced subtree
+    computeShift !sz !sh !min (Balanced _) =
+        let newShift = (countTrailingZeros sz `div` blockShift) * blockShift
         in if newShift > sh then min else newShift
     computeShift _ sh min (Unbalanced arr sizes) =
-        let i' = indexPrimArray sizes (sizeofPrimArray sizes - 1) - indexPrimArray sizes (sizeofPrimArray sizes - 2) -- sizes has at least 2 elements, otherwise the node would be balanced
+        let lastIdx = sizeofPrimArray sizes - 1
+            sz' = indexPrimArray sizes lastIdx - indexPrimArray sizes (lastIdx - 1) -- the size of the last subtree
             newMin = if length arr < blockSize then sh else min
-        in computeShift i' (down sh) newMin (A.last arr)
+        in computeShift sz' (down sh) newMin (A.last arr)
     computeShift _ _ min (Leaf arr) = if length arr < blockSize then 0 else min
 
 -- create a new tree with shift @sh@
-newBranch :: a -> Int -> Tree a
+newBranch :: a -> Shift -> Tree a
 newBranch x = go
   where
-    go 0 = Leaf $ A.singleton x
-    go sh = Balanced $ A.singleton (go (down sh))
-{-# INLINE newBranch #-}
+    go 0 = Leaf (A.singleton x)
+    go sh = Balanced (A.singleton $! go (down sh))
 
 -- splitting
 
diff --git a/src/Data/RRBVector/Internal/Array.hs b/src/Data/RRBVector/Internal/Array.hs
--- a/src/Data/RRBVector/Internal/Array.hs
+++ b/src/Data/RRBVector/Internal/Array.hs
@@ -8,6 +8,7 @@
 module Data.RRBVector.Internal.Array
     ( Array, MutableArray
     , empty, singleton, from2
+    , replicate, replicateSnoc
     , index, head, last
     , update, adjust, adjust'
     , take, drop, splitAt
@@ -24,7 +25,7 @@
 import Control.Monad.ST
 import Data.Foldable (Foldable(..))
 import Data.Primitive.SmallArray
-import Prelude hiding (take, drop, splitAt, head, last, map, traverse, read)
+import Prelude hiding (replicate, take, drop, splitAt, head, last, map, traverse, read)
 
 -- start length array
 data Array a = Array !Int !Int !(SmallArray a)
@@ -40,41 +41,35 @@
         !len' = len1 + len2
 
 instance Foldable Array where
-    foldr f = \z (Array start len arr) ->
-        let !end = start + len
+    foldr f z (Array start len arr) =
+        let end = start + len
             go i
                 | i == end = z
                 | (# x #) <- indexSmallArray## arr i = f x (go (i + 1))
         in go start
-    {-# INLINE foldr #-}
 
-    foldl f = \z (Array start len arr) ->
+    foldl f z (Array start len arr) =
         let go i
                 | i < start = z
                 | (# x #) <- indexSmallArray## arr i = f (go (i - 1)) x
         in go (start + len - 1)
-    {-# INLINE foldl #-}
 
-    foldr' f = \z (Array start len arr) ->
+    foldr' f z (Array start len arr) =
         let go i !acc
                 | i < start = acc
                 | (# x #) <- indexSmallArray## arr i = go (i - 1) (f x acc)
         in go (start + len - 1) z
-    {-# INLINE foldr' #-}
 
-    foldl' f = \z (Array start len arr) ->
-        let !end = start + len
+    foldl' f z (Array start len arr) =
+        let end = start + len
             go i !acc
                 | i == end = acc
                 | (# x #) <- indexSmallArray## arr i = go (i + 1) (f acc x)
         in go start z
-    {-# INLINE foldl' #-}
 
     null arr = length arr == 0
-    {-# INLINE null #-}
 
     length (Array _ len _) = len
-    {-# INLINE length #-}
 
 instance (NFData a) => NFData (Array a) where
     rnf = foldl' (\_ x -> rnf x) ()
@@ -93,6 +88,18 @@
     sma <- newSmallArray 2 x
     writeSmallArray sma 1 y
     pure sma
+
+replicate :: Int -> a -> Array a
+replicate n x = Array 0 n $ runSmallArray (newSmallArray n x)
+
+-- > replicateSnoc n x y = snoc (replicate n x) y
+replicateSnoc :: Int -> a -> a -> Array a
+replicateSnoc n x y = Array 0 len $ runSmallArray $ do
+    sma <- newSmallArray len x
+    writeSmallArray sma n y
+    pure sma
+  where
+    len = n + 1
 
 index :: Array a -> Int -> a
 index (Array start _ arr) idx = indexSmallArray arr (start + idx)
diff --git a/src/Data/RRBVector/Internal/Buffer.hs b/src/Data/RRBVector/Internal/Buffer.hs
--- a/src/Data/RRBVector/Internal/Buffer.hs
+++ b/src/Data/RRBVector/Internal/Buffer.hs
@@ -23,7 +23,7 @@
 push (Buffer buffer offset) x = do
     idx <- readIntRef offset
     A.write buffer idx x
-    modifyIntRef offset (+ 1)
+    writeIntRef offset (idx + 1)
 
 get :: Buffer s a -> ST s (A.Array a)
 get (Buffer buffer offset) = do
diff --git a/src/Data/RRBVector/Internal/IntRef.hs b/src/Data/RRBVector/Internal/IntRef.hs
--- a/src/Data/RRBVector/Internal/IntRef.hs
+++ b/src/Data/RRBVector/Internal/IntRef.hs
@@ -3,7 +3,6 @@
     , newIntRef
     , readIntRef
     , writeIntRef
-    , modifyIntRef
     ) where
 
 import Control.Monad.ST
@@ -14,7 +13,7 @@
 newIntRef :: Int -> ST s (IntRef s)
 newIntRef i = do
     arr <- newPrimArray 1
-    setPrimArray arr 0 1 i
+    writePrimArray arr 0 i
     pure (IntRef arr)
 {-# INLINE newIntRef #-}
 
@@ -25,9 +24,3 @@
 writeIntRef :: IntRef s -> Int -> ST s ()
 writeIntRef (IntRef arr) = writePrimArray arr 0
 {-# INLINE writeIntRef #-}
-
-modifyIntRef :: IntRef s -> (Int -> Int) -> ST s ()
-modifyIntRef (IntRef arr) f = do
-    i <- readPrimArray arr 0
-    writePrimArray arr 0 (f i)
-{-# INLINE modifyIntRef #-}
diff --git a/test/Arbitrary.hs b/test/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/Arbitrary.hs
@@ -0,0 +1,11 @@
+module Arbitrary where
+
+import Test.Tasty.QuickCheck
+
+import qualified Data.RRBVector as V
+import Data.RRBVector.Internal.Debug
+
+-- TODO: improve instance
+instance (Arbitrary a) => Arbitrary (V.Vector a) where
+    arbitrary = oneof [V.fromList <$> arbitrary, fromListUnbalanced <$> arbitrary]
+
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,13 @@
+{-# LANGUAGE BangPatterns #-}
+
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+import Properties (properties)
+import Strictness (strictness)
+
+main :: IO ()
+main = defaultMain . localOption (QuickCheckTests 1000) . localOption (QuickCheckMaxSize 10000) $ testGroup "rrb-vector"
+    [ properties
+    , strictness
+    ]
diff --git a/test/Properties.hs b/test/Properties.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties.hs
@@ -0,0 +1,166 @@
+module Properties
+    ( properties
+    ) where
+
+import Data.Foldable (toList)
+import Data.List (uncons)
+import Prelude hiding ((==)) -- use @===@ instead
+
+import Data.Foldable.WithIndex
+import Data.Functor.WithIndex
+import Data.Traversable.WithIndex
+import qualified Data.RRBVector as V
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+import Arbitrary ()
+
+default (Int)
+
+type V = V.Vector
+
+lookupList :: Int -> [a] -> Maybe a
+lookupList i ls
+    | i < length ls = Just (ls !! i)
+    | otherwise = Nothing
+
+updateList :: Int -> a -> [a] -> [a]
+updateList i x ls
+    | i < length ls = let (left, _ : right) = splitAt i ls in left ++ (x : right)
+    | otherwise = ls
+
+adjustList :: Int -> (a -> a) -> [a] -> [a]
+adjustList i f ls
+    | i < length ls = let (left, x : right) = splitAt i ls in left ++ (f x : right)
+    | otherwise = ls
+
+insertAtList :: Int -> a -> [a] -> [a]
+insertAtList i x ls = let (left, right) = splitAt i ls in left ++ (x : right)
+
+deleteAtList :: Int -> [a] -> [a]
+deleteAtList i ls
+    | i < length ls = let (left, _ : right) = splitAt i ls in left ++ right
+    | otherwise = ls
+
+unsnoc :: [a] -> Maybe ([a], a)
+unsnoc [] = Nothing
+unsnoc ls = Just (init ls, last ls)
+
+instances :: TestTree
+instances = testGroup "instances"
+    [ testGroup "Foldable"
+        [ testProperty "foldr" $ \(v :: V Int) -> foldr (:) [] v === foldr (:) [] (toList v)
+        , testProperty "foldl" $ \(v :: V Int) -> foldl (flip (:)) [] v === foldl (flip (:)) [] (toList v)
+        ]
+    , testGroup "FoldableWithIndex"
+        [ testProperty "ifoldr" $ \(v :: V Int) -> ifoldr (\i x acc -> (i, x) : acc) [] v === ifoldr (\i x acc -> (i, x) : acc) [] (toList v)
+        , testProperty "ifoldl" $ \(v :: V Int) -> ifoldl (\i acc x -> (i, x) : acc) [] v === ifoldl (\i acc x -> (i, x) : acc)  [] (toList v)
+        , testProperty "satisfies `ifoldr (const f) x v = foldr f x v`" $
+            \(v :: V Int) -> ifoldr (const (:)) [] v === foldr (:) [] v
+        , testProperty "satisfies `ifoldl (const f) x v = foldl f x v`" $
+            \(v :: V Int) -> ifoldl (const (flip (:))) [] v === foldl (flip (:)) [] v
+        ]
+    , testGroup "Functor"
+        [ testProperty "fmap" $ \v -> toList (V.map (+ 1) v) === map (+ 1) (toList v)
+        ]
+    , testGroup "FunctorWithIndex"
+        [ testProperty "imap" $ \(v :: V Int) -> toList (imap (,) v) === imap (,) (toList v)
+        , testProperty "satisfies `imap (const f) v = map f v`" $ \v -> imap (const (+ 1)) v === V.map (+ 1) v
+        ]
+    , testGroup "Traversable"
+        [ testProperty "traverse" $
+            \(v :: V Int) -> fmap toList (traverse (Just . (+ 1)) v) === traverse (Just . (+ 1)) (toList v)
+        ]
+    , testGroup "TraversableWithIndex"
+        [ testProperty "itraverse" $
+            \(v :: V Int) -> fmap toList (itraverse (\i x -> Just (i + x)) v) === itraverse (\i x -> Just (i + x)) (toList v)
+        , testProperty "satisfies `itraverse (const f) v = traverse f v`" $
+            \(v :: V Int) -> itraverse (const (Just . (+ 1))) v === traverse (Just . (+ 1)) v
+        ]
+    ]
+
+properties :: TestTree
+properties = testGroup "properties"
+    [ testGroup "fromList"
+        [ testProperty "satisfies `fromList . toList = id`" $ \v -> V.fromList (toList v) === v
+        , testProperty "satisfies `toList . fromList = id`" $ \ls -> toList (V.fromList ls) === ls
+        , testProperty "satisfies `fromList [] = empty`" $ V.fromList [] === V.empty
+        , testProperty "satisfies `fromList [x] = singleton x`" $ \x -> V.fromList [x] === V.singleton x
+        ]
+    , testGroup "replicate"
+        [ testProperty "satisifes `replicate n == fromList . replicate n`" $ \(Positive n) x -> V.replicate n x === V.fromList (replicate n x)
+        , testProperty "returns the empty vector for non-positive n" $ \(NonPositive n) x -> V.replicate n x === V.empty
+        ]
+    , testGroup "<|"
+        [ testProperty "prepends an element" $ \x v -> toList (x V.<| v) === x : toList v
+        , testProperty "works for the empty vector" $ \x -> x V.<| V.empty === V.singleton x
+        ]
+    , testGroup "|>"
+        [ testProperty "appends an element" $ \v x -> toList (v V.|> x) === toList v ++ [x]
+        , testProperty "works for the empty vector" $ \x -> V.empty V.|> x === V.singleton x
+        ]
+    , testGroup "><"
+        [ testProperty "concatenates two vectors" $ \v1 v2 -> toList (v1 V.>< v2) === toList v1 ++ toList v2
+        , testProperty "works for the empty vector" $ \v -> (V.empty V.>< v === v) .&&. (v V.>< V.empty === v)
+        ]
+    , testGroup "lookup"
+        [ testProperty "gets the element at the index" $ \v (NonNegative i) -> V.lookup i v === lookupList i (toList v)
+        , testProperty "returns Nothing for negative indices" $ \v (Negative i) -> V.lookup i v === Nothing
+        ]
+    , testGroup "update"
+        [ testProperty "updates the element at the index" $ \v (NonNegative i) x -> toList (V.update i x v) === updateList i x (toList v)
+        , testProperty "returns the vector for negative indices" $ \v (Negative i) x -> V.update i x v === v
+        ]
+    , testGroup "adjust"
+        [ testProperty "adjusts the element at the index" $ \v (NonNegative i) (Fn f) -> toList (V.adjust i f v) === adjustList i f (toList v)
+        , testProperty "returns the vector for negative indices" $ \v (Negative i) (Fn f) -> V.adjust i f v === v
+        ]
+    , testGroup "adjust'"
+        [ testProperty "adjusts the element at the index" $ \v (NonNegative i) (Fn f) -> toList (V.adjust' i f v) === adjustList i f (toList v)
+        , testProperty "returns the vector for negative indices" $ \v (Negative i) (Fn f) -> V.adjust' i f v === v
+        ]
+    , testGroup "viewl"
+        [ testProperty "works like uncons" $ \v -> fmap (\(x, xs) -> (x, toList xs)) (V.viewl v) === uncons (toList v)
+        , testProperty "works for the empty vector" $ V.viewl V.empty === Nothing
+        ]
+    , testGroup "viewr"
+        [ testProperty "works like unsnoc" $ \v -> fmap (\(xs, x) -> (toList xs, x)) (V.viewr v) === unsnoc (toList v)
+        , testProperty "works for the empty vector" $ V.viewr V.empty === Nothing
+        ]
+    , testGroup "take"
+        [ testProperty "takes n elements" $ \v (Positive n) -> toList (V.take n v) === take n (toList v)
+        , testProperty "returns the empty vector for non-positive n" $ \v (NonPositive n) -> V.take n v === V.empty
+        ]
+    , testGroup "drop"
+        [ testProperty "drops n elements" $ \v (Positive n) -> toList (V.drop n v) === drop n (toList v)
+        , testProperty "returns the vector for non-positive n" $ \v (NonPositive n) -> V.drop n v === v
+        ]
+    , testGroup "splitAt"
+        [ testProperty "splits the vector" $ \v n -> let (v1, v2) = V.splitAt n v in (toList v1, toList v2) === splitAt n (toList v)
+        ]
+    , testGroup "insertAt"
+        [ testProperty "inserts an element" $ \v i x -> toList (V.insertAt i x v) === insertAtList i x (toList v)
+        , testProperty "satisfies `insertAt 0 x v = x <| v`" $ \v x -> V.insertAt 0 x v === x V.<| v
+        , testProperty "satisfies `insertAt (length v) x v = v |> x`" $ \v x -> V.insertAt (length v) x v === v V.|> x
+        ]
+    , testGroup "deleteAt"
+        [ testProperty "deletes an element" $ \v (NonNegative i) -> toList (V.deleteAt i v) === deleteAtList i (toList v)
+        , testProperty "returns the vector for negative indices" $ \v (Negative i) -> V.deleteAt i v === v
+        , testProperty "satisfies `deleteAt 0 v = drop 1 v`" $ \v -> V.deleteAt 0 v === V.drop 1 v
+        , testProperty "satisfies `deleteAt (length v - 1) v = take (length v - 1) v`" $ \v -> V.deleteAt (length v - 1) v === V.take (length v - 1) v
+        ]
+    , testGroup "reverse"
+        [ testProperty "reverses the vector" $ \v -> toList (V.reverse v) === reverse (toList v)
+        ]
+    , testGroup "zip"
+        [ testProperty "zips two vectors" $ \v1 v2 -> toList (V.zip v1 v2) === zip (toList v1) (toList v2)
+        ]
+    , testGroup "zipWith"
+        [ testProperty "zips two vectors with a function" $ \v1 v2 -> toList (V.zipWith (+) v1 v2) === zipWith (+) (toList v1) (toList v2)
+        , testProperty "satisfies `zipWith (,) v1 v2 = zip v1 v2`" $ \v1 v2 -> V.zipWith (,) v1 v2 === V.zip v1 v2
+        ]
+    , testGroup "unzip"
+        [ testProperty "unzips the vector" $ \v -> (\(xs, ys) -> (toList xs, toList ys)) (V.unzip v) === unzip (toList v)
+        ]
+    , instances
+    ]
diff --git a/test/Spec.hs b/test/Spec.hs
deleted file mode 100644
--- a/test/Spec.hs
+++ /dev/null
@@ -1,83 +0,0 @@
-import Data.Foldable (toList)
-import Data.List (uncons)
-
-import Test.Hspec
-import Test.Hspec.QuickCheck
-import Test.QuickCheck
-
-import qualified Data.RRBVector as V
-import Data.RRBVector.Internal.Debug (fromListUnbalanced)
-
-default (Int)
-
-instance (Arbitrary a) => Arbitrary (V.Vector a) where
-    arbitrary = oneof [V.fromList <$> arbitrary, fromListUnbalanced <$> arbitrary]
-
-lookupList :: Int -> [a] -> Maybe a
-lookupList i ls
-    | i < length ls = Just (ls !! i)
-    | otherwise = Nothing
-
-updateList :: Int -> a -> [a] -> [a]
-updateList i x ls
-    | i < length ls = let (left, _ : right) = splitAt i ls in left ++ (x : right)
-    | otherwise = ls
-
-adjustList :: Int -> (a -> a) -> [a] -> [a]
-adjustList i f ls
-    | i < length ls = let (left, x : right) = splitAt i ls in left ++ (f x : right)
-    | otherwise = ls
-
-unsnoc :: [a] -> Maybe ([a], a)
-unsnoc [] = Nothing
-unsnoc ls = Just (init ls, last ls)
-
-main :: IO ()
-main = hspec . modifyMaxSuccess maxN . modifyMaxSize maxN $ do
-    prop "satisfies `fromList . toList == id`" $ \v -> V.fromList (toList v) === v
-    prop "satisfies `toList . fromList == id`" $ \ls -> toList (V.fromList ls) === ls
-
-    describe "lookup" $ do
-        prop "gets the element at the index" $ \v (NonNegative i) -> V.lookup i v === lookupList i (toList v)
-        prop "returns Nothing for negative indices" $ \v (Negative i) -> V.lookup i v === Nothing
-
-    describe "update" $ do
-        prop "updates the element at the index" $ \v (NonNegative i) x -> toList (V.update i x v) === updateList i x (toList v)
-        prop "returns the vector for negative indices" $ \v (Negative i) x -> V.update i x v === v
-
-    describe "adjust" $ do
-        prop "adjusts the element at the index" $ \v (NonNegative i) -> toList (V.adjust i (+ 1) v) === adjustList i (+ 1) (toList v)
-        prop "returns the vector for negative indices" $ \v (Negative i) -> V.adjust i (+ 1) v === v
-
-    describe "><" $ do
-        prop "concatenates two vectors" $ \v1 v2 -> toList (v1 V.>< v2) === toList v1 ++ toList v2
-        prop "works for the empty vector" $ \v -> (V.empty V.>< v `shouldBe` v) .&&. (v V.>< V.empty `shouldBe` v)
-
-    describe "|>" $ do
-        prop "appends an element" $ \v x -> toList (v V.|> x) === toList v ++ [x]
-        prop "works for the empty vector" $ \x -> V.empty V.|> x `shouldBe` V.singleton x
-
-    describe "<|" $ do
-        prop "prepends an element" $ \x v -> toList (x V.<| v) === x : toList v
-        prop "works for the empty vector" $ \x -> x V.<| V.empty `shouldBe` V.singleton x
-
-    describe "take" $ do
-        prop "takes n elements" $ \v (Positive n) -> toList (V.take n v) === take n (toList v)
-        prop "returns the empty vector for non-positive n" $ \v (NonPositive n) -> V.take n v === V.empty
-
-    describe "drop" $ do
-        prop "drops n elements" $ \v (Positive n) -> toList (V.drop n v) === drop n (toList v)
-        prop "does nothing for non-positive n" $ \v (NonPositive n) -> V.drop n v === v
-
-    describe "viewl" $ do
-        prop "works like uncons" $ \v -> fmap (\(x, xs) -> (x, toList xs)) (V.viewl v) === uncons (toList v)
-        prop "works for the empty vector" $ V.viewl V.empty `shouldBe` Nothing
-
-    describe "viewr" $ do
-        prop "works like unsnoc" $ \v -> fmap (\(xs, x) -> (toList xs, x)) (V.viewr v) === unsnoc (toList v)
-        prop "works for the empty vector" $ V.viewr V.empty `shouldBe` Nothing
-
-    describe "map" $ do
-        prop "maps over the vector" $ \v -> toList (V.map (+ 1) v) === map (+ 1) (toList v)
-  where
-    maxN = const 10_000
diff --git a/test/Strictness.hs b/test/Strictness.hs
new file mode 100644
--- /dev/null
+++ b/test/Strictness.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP #-}
+
+#define GHC_HEAP_VIEW defined(VERSION_ghc_heap_view)
+
+module Strictness
+    ( strictness
+    ) where
+
+#if GHC_HEAP_VIEW
+import Control.DeepSeq (deepseq)
+import GHC.AssertNF (isNF)
+#endif
+import qualified Data.RRBVector as V
+import Test.Tasty
+import Test.Tasty.QuickCheck
+
+import Arbitrary ()
+
+default (Int)
+
+#if GHC_HEAP_VIEW
+testNF :: a -> Property
+testNF !x = ioProperty (isNF x)
+
+tailVector :: V.Vector a -> Maybe (V.Vector a)
+tailVector v = case V.viewl v of
+    Nothing -> Nothing
+    Just (_, xs) -> Just xs
+
+initVector :: V.Vector a -> Maybe (V.Vector a)
+initVector v = case V.viewr v of
+    Nothing -> Nothing
+    Just (xs, _) -> Just xs
+#endif
+
+strictness :: TestTree
+strictness = testGroup "strictness"
+#if GHC_HEAP_VIEW
+    [ testGroup "nf"
+        [ testProperty "empty" $ testNF V.empty
+        , testProperty "singleton" $ testNF (V.singleton 42)
+        , testProperty "fromList" $ \ls -> ls `deepseq` testNF (V.fromList ls)
+        , testProperty "replicate" $ \n -> testNF (V.replicate n 42)
+        , testProperty "update" $ \v (NonNegative i) -> v `deepseq` testNF (V.update i 42 v)
+        , testProperty "adjust'" $ \v (NonNegative i) -> v `deepseq` testNF (V.adjust' i (+ 1) v)
+        , testProperty "<|" $ \v -> v `deepseq` testNF (42 V.<| v)
+        , testProperty "|>" $ \v -> v `deepseq` testNF (v V.|> 42)
+        , testProperty "><" $ \v1 v2 -> v1 `deepseq` v2 `deepseq` testNF (v1 V.>< v2)
+        , testProperty "take" $ \v n -> v `deepseq` testNF (V.take n v)
+        , testProperty "drop" $ \v n -> v `deepseq` testNF (V.drop n v)
+        , testProperty "splitAt" $ \v n -> v `deepseq` testNF (V.splitAt n v)
+        , testProperty "insertAt" $ \v i -> v `deepseq` testNF (V.insertAt i 42 v)
+        , testProperty "deleteAt" $ \v i -> v `deepseq` testNF (V.deleteAt i v)
+        , testProperty "viewl (tail)" $ \v -> v `deepseq` testNF (tailVector v)
+        , testProperty "viewr (init)" $ \v -> v `deepseq` testNF (initVector v)
+        , testProperty "reverse" $ \v -> v `deepseq` testNF (V.reverse v)
+        ]
+    , testGroup "bottom"
+#else
+    [ testGroup "bottom"
+#endif
+        [ testProperty "singleton" $ V.singleton undefined `seq` ()
+        , testProperty "fromList" $ \n -> V.fromList (replicate n undefined) `seq` ()
+        , testProperty "replicate" $ \n -> V.replicate n undefined `seq` ()
+        , testProperty "<|" $ \v -> undefined V.<| v `seq` ()
+        , testProperty "|>" $ \v -> v V.|> undefined `seq` ()
+        , testProperty "update" $ \v i -> V.update i undefined v `seq` ()
+        , testProperty "adjust" $ \v i -> V.adjust i (const undefined) v `seq` ()
+        , testProperty "insertAt" $ \v i -> V.insertAt i undefined v `seq` ()
+        , testProperty "map" $ \v -> V.map (const undefined) v `seq` ()
+        ]
+    ]
