diff --git a/test/test_kernels.py b/test/test_kernels.py
index 2ddbd9c5f804269b3c91ae3cad323f6453fa2a65..d1768fd7c5040615a993f1ebb105b9bb1498c5b3 100644
--- a/test/test_kernels.py
+++ b/test/test_kernels.py
@@ -22,6 +22,7 @@ THE SOFTWARE.
 
 import pytest
 import sys
+from functools import partial
 
 import numpy as np
 import numpy.linalg as la
@@ -51,6 +52,8 @@ from sumpy.kernel import (
     AxisTargetDerivative,
     DirectionalSourceDerivative)
 
+import sumpy.toys as t
+
 import logging
 logger = logging.getLogger(__name__)
 
@@ -499,8 +502,6 @@ def test_translations(actx_factory, knl, local_expn_class, mpole_expn_class,
     res = 20
     nsources = 15
 
-    target_kernels = [knl]
-
     extra_kwargs = {}
     if isinstance(knl, HelmholtzKernel):
         extra_kwargs["k"] = 0.05
@@ -510,11 +511,10 @@ def test_translations(actx_factory, knl, local_expn_class, mpole_expn_class,
     # Just to make sure things also work away from the origin
     rng = np.random.default_rng(18)
     origin = np.array([2, 1, 0][:knl.dim], np.float64)
-    sources = actx.from_numpy(
+    sources = (
         0.7 * (-0.5 + rng.random((knl.dim, nsources), dtype=np.float64))
         + origin[:, np.newaxis])
-    strengths = actx.from_numpy(
-        np.ones(nsources, dtype=np.float64) * (1/nsources))
+    strengths = np.ones(nsources, dtype=np.float64) * (1/nsources)
 
     pconv_verifier_p2m2p = PConvergenceVerifier()
     pconv_verifier_p2m2m2p = PConvergenceVerifier()
@@ -525,8 +525,9 @@ def test_translations(actx_factory, knl, local_expn_class, mpole_expn_class,
 
     eval_offset = np.array([5.5, 0.0, 0][:knl.dim])
     fp = FieldPlotter(eval_offset + origin, extent=0.3, npoints=res)
+    targets = fp.points
 
-    centers = actx.from_numpy((np.array(
+    centers = (np.array(
             [
                 # box 0: particles, first mpole here
                 [0, 0, 0][:knl.dim],
@@ -540,7 +541,7 @@ def test_translations(actx_factory, knl, local_expn_class, mpole_expn_class,
                 # box 3: second local and eval here
                 eval_offset
                 ],
-            dtype=np.float64) + origin).T.copy())
+            dtype=np.float64) + origin).T.copy()
 
     del eval_offset
 
@@ -549,192 +550,50 @@ def test_translations(actx_factory, knl, local_expn_class, mpole_expn_class,
     else:
         orders = [3, 4, 5]
 
-    nboxes = centers.shape[-1]
-
-    def eval_at(e2p, source_box_nr, rscale):
-        e2p_target_boxes = actx.from_numpy(
-            np.array([source_box_nr], dtype=np.int32))
-
-        # These are indexed by global box numbers.
-        e2p_box_target_starts = actx.from_numpy(
-            np.array([0, 0, 0, 0], dtype=np.int32))
-        e2p_box_target_counts_nonchild = actx.from_numpy(
-            np.array([0, 0, 0, 0], dtype=np.int32))
-        e2p_box_target_counts_nonchild[source_box_nr] = fp.points.shape[-1]
-
-        evt, (pot,) = e2p(
-                actx.queue,
-
-                src_expansions=mpoles,
-                src_base_ibox=0,
-
-                target_boxes=e2p_target_boxes,
-                box_target_starts=e2p_box_target_starts,
-                box_target_counts_nonchild=e2p_box_target_counts_nonchild,
-                centers=centers,
-                targets=targets,
-
-                rscale=rscale,
-                **extra_kwargs
-                )
-        pot = actx.to_numpy(pot)
-
-        return pot
-
     m2l_factory = NonFFTM2LTranslationClassFactory()
     m2l_translation = m2l_factory.get_m2l_translation_class(knl, local_expn_class)()
 
-    for order in orders:
-        m_expn = mpole_expn_class(knl, order=order)
-        l_expn = local_expn_class(knl, order=order, m2l_translation=m2l_translation)
-
-        from sumpy import P2EFromSingleBox, E2PFromSingleBox, P2P, E2EFromCSR
-        p2m = P2EFromSingleBox(actx.context, m_expn)
-        m2m = E2EFromCSR(actx.context, m_expn, m_expn)
-        m2p = E2PFromSingleBox(actx.context, m_expn, target_kernels)
-        m2l = E2EFromCSR(actx.context, m_expn, l_expn)
-        l2l = E2EFromCSR(actx.context, l_expn, l_expn)
-        l2p = E2PFromSingleBox(actx.context, l_expn, target_kernels)
-        p2p = P2P(actx.context, target_kernels, exclude_self=False)
-
-        targets = actx.from_numpy(make_obj_array(fp.points))
-
-        # {{{ compute (direct) reference solution
-
-        evt, (pot_direct,) = p2p(
-                actx.queue,
-                targets, sources, (strengths,),
-                **extra_kwargs)
-        pot_direct = actx.to_numpy(pot_direct)
-
-        # }}}
-
-        m1_rscale = 0.5
-        m2_rscale = 0.25
-        l1_rscale = 0.5
-        l2_rscale = 0.25
-
-        # {{{ apply P2M
-
-        p2m_source_boxes = actx.from_numpy(np.array([0], dtype=np.int32))
+    toy_ctx = t.ToyContext(
+            actx.context,
+            kernel=knl,
+            local_expn_class=partial(local_expn_class,
+                m2l_translation=m2l_translation),
+            mpole_expn_class=mpole_expn_class,
+            extra_kernel_kwargs=extra_kwargs,
+    )
 
-        # These are indexed by global box numbers.
-        p2m_box_source_starts = actx.from_numpy(
-            np.array([0, 0, 0, 0], dtype=np.int32))
-        p2m_box_source_counts_nonchild = actx.from_numpy(
-            np.array([nsources, 0, 0, 0], dtype=np.int32))
+    p = t.PointSources(toy_ctx, sources, weights=strengths)
+    p2p = p.eval(targets)
 
-        evt, (mpoles,) = p2m(actx.queue,
-                source_boxes=p2m_source_boxes,
-                box_source_starts=p2m_box_source_starts,
-                box_source_counts_nonchild=p2m_box_source_counts_nonchild,
-                centers=centers,
-                sources=sources,
-                strengths=(strengths,),
-                nboxes=nboxes,
-                rscale=m1_rscale,
-
-                tgt_base_ibox=0,
-                **extra_kwargs)
-
-        # }}}
-
-        pot = eval_at(m2p, 0, m1_rscale)
-
-        err = la.norm((pot - pot_direct) / res**2)
-        err = err / (la.norm(pot_direct) / res**2)
+    m1_rscale = 0.5
+    m2_rscale = 0.25
+    l1_rscale = 0.5
+    l2_rscale = 0.25
 
+    for order in orders:
+        print(centers[:, 0].shape)
+        p2m = t.multipole_expand(p, centers[:, 0], order, m1_rscale)
+        p2m2p = p2m.eval(targets)
+        err = la.norm((p2m2p - p2p) / res**2)
+        err = err / (la.norm(p2p) / res**2)
         pconv_verifier_p2m2p.add_data_point(order, err)
 
-        # {{{ apply M2M
-
-        m2m_target_boxes = actx.from_numpy(np.array([1], dtype=np.int32))
-        m2m_src_box_starts = actx.from_numpy(np.array([0, 1], dtype=np.int32))
-        m2m_src_box_lists = actx.from_numpy(np.array([0], dtype=np.int32))
-
-        evt, (mpoles,) = m2m(actx.queue,
-                src_expansions=mpoles,
-                src_base_ibox=0,
-                tgt_base_ibox=0,
-                ntgt_level_boxes=mpoles.shape[0],
-
-                target_boxes=m2m_target_boxes,
-
-                src_box_starts=m2m_src_box_starts,
-                src_box_lists=m2m_src_box_lists,
-                centers=centers,
-
-                src_rscale=m1_rscale,
-                tgt_rscale=m2_rscale,
-                **extra_kwargs)
-
-        # }}}
-
-        pot = eval_at(m2p, 1, m2_rscale)
-
-        err = la.norm((pot - pot_direct)/res**2)
-        err = err / (la.norm(pot_direct) / res**2)
-
+        p2m2m = t.multipole_expand(p2m, centers[:, 1], order, m2_rscale)
+        p2m2m2p = p2m2m.eval(targets)
+        err = la.norm((p2m2m2p - p2p)/res**2)
+        err = err / (la.norm(p2p) / res**2)
         pconv_verifier_p2m2m2p.add_data_point(order, err)
 
-        # {{{ apply M2L
-
-        m2l_target_boxes = actx.from_numpy(np.array([2], dtype=np.int32))
-        m2l_src_box_starts = actx.from_numpy(np.array([0, 1], dtype=np.int32))
-        m2l_src_box_lists = actx.from_numpy(np.array([1], dtype=np.int32))
-
-        evt, (mpoles,) = m2l(actx.queue,
-                src_expansions=mpoles,
-                src_base_ibox=0,
-                tgt_base_ibox=0,
-                ntgt_level_boxes=mpoles.shape[0],
-
-                target_boxes=m2l_target_boxes,
-                src_box_starts=m2l_src_box_starts,
-                src_box_lists=m2l_src_box_lists,
-                centers=centers,
-
-                src_rscale=m2_rscale,
-                tgt_rscale=l1_rscale,
-                **extra_kwargs)
-
-        # }}}
-
-        pot = eval_at(l2p, 2, l1_rscale)
-
-        err = la.norm((pot - pot_direct)/res**2)
-        err = err / (la.norm(pot_direct) / res**2)
-
+        p2m2m2l = t.local_expand(p2m2m, centers[:, 2], order, l1_rscale)
+        p2m2m2l2p = p2m2m2l.eval(targets)
+        err = la.norm((p2m2m2l2p - p2p)/res**2)
+        err = err / (la.norm(p2p) / res**2)
         pconv_verifier_p2m2m2l2p.add_data_point(order, err)
 
-        # {{{ apply L2L
-
-        l2l_target_boxes = actx.from_numpy(np.array([3], dtype=np.int32))
-        l2l_src_box_starts = actx.from_numpy(np.array([0, 1], dtype=np.int32))
-        l2l_src_box_lists = actx.from_numpy(np.array([2], dtype=np.int32))
-
-        evt, (mpoles,) = l2l(actx.queue,
-                src_expansions=mpoles,
-                src_base_ibox=0,
-                tgt_base_ibox=0,
-                ntgt_level_boxes=mpoles.shape[0],
-
-                target_boxes=l2l_target_boxes,
-                src_box_starts=l2l_src_box_starts,
-                src_box_lists=l2l_src_box_lists,
-                centers=centers,
-
-                src_rscale=l1_rscale,
-                tgt_rscale=l2_rscale,
-                **extra_kwargs)
-
-        # }}}
-
-        pot = eval_at(l2p, 3, l2_rscale)
-
-        err = la.norm((pot - pot_direct)/res**2)
-        err = err / (la.norm(pot_direct) / res**2)
-
+        p2m2m2l2l = t.local_expand(p2m2m2l, centers[:, 3], order, l2_rscale)
+        p2m2m2l2l2p = p2m2m2l2l.eval(targets)
+        err = la.norm((p2m2m2l2l2p - p2p)/res**2)
+        err = err / (la.norm(p2p) / res**2)
         pconv_verifier_full.add_data_point(order, err)
 
     for name, verifier in [