<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
  <title>Testing on blog.yarwood.me.uk</title>
  <link>https://blog.yarwood.me.uk/tags/testing/</link>
  <description>Recent content in Testing on blog.yarwood.me.uk</description>
  <generator>Hugo -- gohugo.io</generator>
  <language>en</language>
  <lastBuildDate>Tue, 20 Oct 2026 16:00:00 +0000</lastBuildDate>
  
    <atom:link href="https://blog.yarwood.me.uk/tags/testing/index.xml" rel="self" type="application/rss+xml" />
  
  
    <item>
      <title>Testing KubeVirt controllers in seconds, not clusters</title>
      <link>https://blog.yarwood.me.uk/2026/10/20/kubevirt-integration-test-framework/</link>
      <pubDate>Tue, 20 Oct 2026 16:00:00 +0000</pubDate>
      <guid>https://blog.yarwood.me.uk/2026/10/20/kubevirt-integration-test-framework/</guid>
      <description>&lt;figure&gt;&lt;img src=&#34;https://blog.yarwood.me.uk/img/KubeVirt_logo.png&#34;&gt;
&lt;/figure&gt;

&lt;p&gt;KubeVirt&amp;rsquo;s new integration test framework runs a &lt;strong&gt;real kube-apiserver and etcd locally, with real controllers in-process&lt;/strong&gt;. Tests exercise VM → VMI → Pod → Scheduled in seconds, without deploying a cluster or booting a VM.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/kubevirt/kubevirt/pull/18726&#34;&gt;base framework&lt;/a&gt;, &lt;a href=&#34;https://github.com/kubevirt/kubevirt/pull/18841&#34;&gt;builder update&lt;/a&gt; and &lt;a href=&#34;https://github.com/kubevirt/project-infra/pull/5478&#34;&gt;non-voting presubmit&lt;/a&gt; are merged. &lt;a href=&#34;https://github.com/kubevirt/enhancements/issues/371&#34;&gt;VEP #371&lt;/a&gt; targets alpha in v1.10.0; further coverage and webhook support remain under review.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;
&lt;p&gt;The instancetype ControllerRevision upgrade tests were my starting point: pure controller logic, no guest required, yet running them meant paying the cost of the functional suite.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&#34;https://blog.yarwood.me.uk/img/kubevirt_itf_tiers.png&#34;
			alt=&#34;Each tier adds fidelity and cost. Integration tests exercise real controller interactions without cluster setup.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Each tier adds fidelity and cost. Integration tests exercise real controller interactions without cluster setup.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In controller unit tests built around GoMock, Controller A&amp;rsquo;s writes don&amp;rsquo;t become API-server state that Controller B can observe. Functional tests exercise those interactions but require a deployed cluster. This tier gives controllers a real API server, catching reconciliation bugs and status-update races with a shorter feedback loop. The VEP has more &lt;a href=&#34;https://github.com/kubevirt/enhancements/blob/main/veps/sig-control-plane/371-envtest-framework/vep.md#motivation&#34;&gt;concrete examples&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-idea-shamelessly-repeated&#34;&gt;The idea: shamelessly repeated&lt;/h2&gt;
&lt;p&gt;I spent years in OpenStack Nova before KubeVirt, and borrowed this pattern from its &lt;a href=&#34;https://docs.openstack.org/nova/latest/contributor/testing.html#functional-tests&#34;&gt;functional test framework&lt;/a&gt;: &lt;strong&gt;real services, fake external dependencies&lt;/strong&gt;. Nova runs its API, conductor and scheduler against a real database, with a fake hypervisor driver and &lt;a href=&#34;https://github.com/openstack/nova/blob/master/nova/tests/functional/integrated_helpers.py&#34;&gt;fixtures for Neutron, Cinder and Glance&lt;/a&gt;. &lt;a href=&#34;https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/envtest&#34;&gt;envtest&lt;/a&gt; makes the equivalent approach possible for KubeVirt.&lt;/p&gt;
&lt;h2 id=&#34;how-it-works&#34;&gt;How it works&lt;/h2&gt;
&lt;figure&gt;&lt;img src=&#34;https://blog.yarwood.me.uk/img/kubevirt_itf_architecture.png&#34;
			alt=&#34;Real VM and VMI controllers reconcile against envtest; the pod simulator supplies pod status updates.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;Real VM and VMI controllers reconcile against envtest; the pod simulator supplies pod status updates.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;ul&gt;
&lt;li&gt;CRDs come from virt-operator&amp;rsquo;s Go definitions. Real clients and API-server semantics provide OpenAPI validation, status subresource enforcement and &lt;code&gt;resourceVersion&lt;/code&gt; conflicts.&lt;/li&gt;
&lt;li&gt;The framework &lt;a href=&#34;https://github.com/kubevirt/kubevirt/blob/main/tests/integration/framework/framework.go&#34;&gt;calls production controller constructors directly&lt;/a&gt;, catching signature changes at compile time. Its own wiring uses dummy CDI informers, an instancetype stub, and no-op firmware and network synchronisation.&lt;/li&gt;
&lt;li&gt;envtest has no kubelet, scheduler or kube-controller-manager. The pod simulator binds pods to a fake node and marks them Running and Ready, allowing VMIs to reach Scheduled. Explicit resource cleanup compensates for the missing namespace and garbage-collection controllers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The merged base has no KubeVirt admission webhooks. &lt;a href=&#34;https://github.com/kubevirt/kubevirt/pull/18729&#34;&gt;#18729&lt;/a&gt; adds the real VM admission handlers, enabled by default with an opt-out for schema-only tests.&lt;/p&gt;
&lt;h2 id=&#34;what-a-test-looks-like&#34;&gt;What a test looks like&lt;/h2&gt;
&lt;figure&gt;&lt;img src=&#34;https://blog.yarwood.me.uk/img/kubevirt_itf_lifecycle.png&#34;
			alt=&#34;VM → VMI → Pod → Scheduled, with the pod simulator closing the loop.&#34;&gt;&lt;figcaption&gt;
			&lt;p&gt;VM → VMI → Pod → Scheduled, with the pod simulator closing the loop.&lt;/p&gt;
		&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Adapted from the &lt;a href=&#34;https://github.com/kubevirt/kubevirt/blob/main/tests/integration/vm_lifecycle_test.go&#34;&gt;merged lifecycle test&lt;/a&gt;, with imports omitted. &lt;code&gt;Start&lt;/code&gt; registers suite shutdown; &lt;code&gt;CreateNamespace&lt;/code&gt; registers per-test resource cleanup.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;framework&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Framework&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;BeforeSuite&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;framework&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Start&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Background&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;Describe&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;VM Lifecycle&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;It&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;creates a Scheduled VMI from a VM&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SpecContext&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;ns&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CreateNamespace&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;vm&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;libvmi&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewVirtualMachine&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;libvmi&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;libvmi&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithMemoryRequest&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;128Mi&amp;#34;&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;libvmi&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithRunStrategy&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;virtv1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;RunStrategyAlways&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;vm&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;f&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;VirtClient&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;VirtualMachine&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ns&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Create&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;vm&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;metav1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CreateOptions&lt;/span&gt;{})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;Expect&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;NotTo&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;HaveOccurred&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;Eventually&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;matcher&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ThisVMIWith&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ns&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;vm&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Name&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Millisecond&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		).&lt;span style=&#34;color:#a6e22e&#34;&gt;Should&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;matcher&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;BeInPhase&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;virtv1&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Scheduled&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This single-test lifecycle suite runs in roughly &lt;strong&gt;16–20 seconds locally&lt;/strong&gt;, including API-server startup and cleanup, as &lt;a href=&#34;https://github.com/kubevirt/kubevirt/pull/18726&#34;&gt;reported in the base PR&lt;/a&gt;. Tests reuse the functional suite&amp;rsquo;s client interface and matchers, so migrations can retain assertions while replacing cluster setup with framework startup and isolated fixtures.&lt;/p&gt;
&lt;p&gt;In a separate &lt;a href=&#34;https://github.com/kubevirt/enhancements/blob/main/veps/sig-control-plane/371-envtest-framework/poc.md#functional-test-migration-benchmark&#34;&gt;PoC benchmark&lt;/a&gt;, eight instancetype ControllerRevision upgrade tests ran &lt;strong&gt;13.4x faster&lt;/strong&gt;: 152.6s in the functional sig-compute lane versus 11.4s with shared envtest. The benchmark doesn&amp;rsquo;t specify whether startup and cleanup are included.&lt;/p&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s next&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/kubevirt/enhancements/blob/main/veps/sig-control-plane/371-envtest-framework/vep.md#graduation-requirements&#34;&gt;VEP&amp;rsquo;s graduation criteria&lt;/a&gt; describe the remaining work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alpha (v1.10.0)&lt;/strong&gt; — remaining coverage for instancetypes, generation tracking, webhooks, schema/CEL validation and pod failures (&lt;a href=&#34;https://github.com/kubevirt/kubevirt/pull/18727&#34;&gt;#18727&lt;/a&gt;–&lt;a href=&#34;https://github.com/kubevirt/kubevirt/pull/18731&#34;&gt;#18731&lt;/a&gt;); non-voting CI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Beta&lt;/strong&gt; — fake libvirt gRPC and virt-handler simulation so VMIs reach &lt;code&gt;Running&lt;/code&gt;, workload-update controller support, and further test migrations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GA&lt;/strong&gt; — wider operator, migration and storage coverage, contributor guidance, and required integration tests for controller changes. CI becomes &lt;strong&gt;voting&lt;/strong&gt; after zero flakes across multiple release cycles with representative coverage.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;what-this-doesnt-replace&#34;&gt;What this doesn&amp;rsquo;t replace&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s no hypervisor, networking dataplane or guest OS here. Infrastructure tests stay in the functional suite, and overlapping cluster-based coverage remains necessary to catch simulation drift. Tests needing components the framework currently stubs also need further support before migrating.&lt;/p&gt;
&lt;p&gt;Try &lt;code&gt;make test-integration&lt;/code&gt; from a KubeVirt checkout. It uses the standard builder container, with envtest binaries included. The lifecycle test provides a starting point for your own controller coverage.&lt;/p&gt;
</description>
    </item>
  
  </channel>
</rss>
