<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alon Farchy</title>
    <description>The latest articles on DEV Community by Alon Farchy (@virtualmaker).</description>
    <link>https://dev.to/virtualmaker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2583174%2Fcc44555d-ab79-4524-9051-200c44011760.png</url>
      <title>DEV Community: Alon Farchy</title>
      <link>https://dev.to/virtualmaker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virtualmaker"/>
    <language>en</language>
    <item>
      <title>Deploying Unity Builds to Google Play Store with Buildalon</title>
      <dc:creator>Alon Farchy</dc:creator>
      <pubDate>Mon, 16 Feb 2026 16:03:21 +0000</pubDate>
      <link>https://dev.to/virtualmaker/deploying-unity-builds-to-google-play-store-with-buildalon-4d6</link>
      <guid>https://dev.to/virtualmaker/deploying-unity-builds-to-google-play-store-with-buildalon-4d6</guid>
      <description>&lt;p&gt;Getting your game onto the Google Play Store involves a complex dance of signing keys, version codes, and console configurations. Manually building an Android App Bundle (AAB), signing it, and uploading it to the Play Console every time you want to test a change is a recipe for burnout.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through how to automate your Unity builds for &lt;strong&gt;Android&lt;/strong&gt; and deploy them directly to the &lt;strong&gt;Google Play Store&lt;/strong&gt; internal testing track using GitHub Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Setup Overview&lt;/li&gt;
&lt;li&gt;Setting up Google Play Console&lt;/li&gt;
&lt;li&gt;Configuring Google Cloud Access&lt;/li&gt;
&lt;li&gt;Handling the Keystore&lt;/li&gt;
&lt;li&gt;Updating the Build Pipeline&lt;/li&gt;
&lt;li&gt;Deploying to Internal Testing&lt;/li&gt;
&lt;li&gt;Installing the Internal App and Inviting Testers&lt;/li&gt;
&lt;li&gt;About Buildalon&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Google Play Developer Account&lt;/strong&gt;: You need to be able to access the &lt;a href="https://play.google.com/console/signup" rel="noopener noreferrer"&gt;Google Play Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Working Unity Build&lt;/strong&gt;: You should have a basic automated build pipeline set up. Check out our &lt;a href="https://dev.to/blog/automating-unity-builds-with-github-actions"&gt;GitHub Actions Unity&lt;/a&gt; guide if you're starting from scratch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup Overview
&lt;/h2&gt;

&lt;p&gt;Automating Android deployments involves two key security components: &lt;strong&gt;Android Keystores&lt;/strong&gt; (for &lt;strong&gt;Android Signing&lt;/strong&gt;) and &lt;strong&gt;Service Accounts&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;App Registration&lt;/strong&gt;: Create the app placeholder in Google Play Console.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Authentication&lt;/strong&gt;: We'll use &lt;strong&gt;Workload Identity Federation&lt;/strong&gt; to securely authenticate &lt;strong&gt;GitHub Actions Android&lt;/strong&gt; workflows with Google Cloud.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Keystore&lt;/strong&gt;: We need to securely provide our signing keystore to the build server.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Build &amp;amp; Upload&lt;/strong&gt;: We'll update the workflow to build an &lt;strong&gt;Android App Bundle&lt;/strong&gt; (&lt;code&gt;.aab&lt;/code&gt;) and upload it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setting up Google Play Console
&lt;/h2&gt;

&lt;p&gt;First, let's make sure Google is expecting our app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Log in to &lt;a href="https://play.google.com/console" rel="noopener noreferrer"&gt;Google Play Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Create app&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Fill in the app details (Name, Default language, Game/App type).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Important:&lt;/strong&gt; Manually build your APK/AAB in Unity and upload it to the &lt;strong&gt;Internal Testing&lt;/strong&gt; track via the web interface. This initializes the track, sets the &lt;strong&gt;Package Name&lt;/strong&gt; permanently, and allows you to answer the initial regulatory questions (Data Safety, etc.).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configuring Google Cloud Access
&lt;/h2&gt;

&lt;p&gt;To allow GitHub Actions to upload via the API, we need to link our Google Play account to a Google Cloud project. We will use a modern, secure method called &lt;strong&gt;Workload Identity Federation&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enable the Google Play Android Developer API
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Go to the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Select or create a project for your game.&lt;/li&gt;
&lt;li&gt; Search for &lt;strong&gt;Google Play Android Developer API&lt;/strong&gt; and enable it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf65rhxmwdfsr96xvvs5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf65rhxmwdfsr96xvvs5.webp" alt="Google Cloud Play API" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Service Account
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; In Google Cloud Console, go to &lt;strong&gt;IAM &amp;amp; Admin&lt;/strong&gt; &amp;gt; &lt;strong&gt;Service Accounts&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Create Service Account&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblglgnajhptww213zv1r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblglgnajhptww213zv1r.webp" alt="Google Cloud Service Accounts" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Name it something like &lt;code&gt;unity-cicd-uploader&lt;/code&gt; and click &lt;strong&gt;Create and Continue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Skip&lt;/strong&gt; the role assignment steps (click &lt;strong&gt;Continue&lt;/strong&gt; then &lt;strong&gt;Done&lt;/strong&gt;). This service account does not need specific Google Cloud permissions, only Google Play permissions.&lt;/li&gt;
&lt;li&gt; Copy the email address (e.g., &lt;code&gt;unity-cicd-uploader@my-project.iam.gserviceaccount.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; Go back to &lt;strong&gt;Google Play Console&lt;/strong&gt; &amp;gt; &lt;strong&gt;Users and permissions&lt;/strong&gt; and invite this email.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwzorgeptpfpabl10398.webp" alt="Google Play Console Users and Permissions" width="800" height="371"&gt;
&lt;/li&gt;
&lt;li&gt; In the permissions tab, grant it &lt;strong&gt;App Access&lt;/strong&gt; to your game, and under &lt;strong&gt;Account permissions&lt;/strong&gt;, ensure it has &lt;strong&gt;Releases to play store&lt;/strong&gt; &amp;gt; &lt;strong&gt;Release to testing tracks&lt;/strong&gt; (or Admin if you want full control).
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ip3k5fc3luhlejh68bg.webp" alt="Google Play Console Release Options" width="800" height="347"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Setup Workload Identity Federation
&lt;/h3&gt;

&lt;p&gt;Next we need to give have the action runner authenticate with the service account we created. A secure way to do this is to use Workload Identity Federation, which procides an secure authentication scheme between GitHub Actions and Google Cloud.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;In Google Cloud Console, go to &lt;strong&gt;IAM &amp;amp; Admin&lt;/strong&gt; &amp;gt; &lt;strong&gt;Workload Identity Federation&lt;/strong&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgyb79hu06xnigrjuq96s.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgyb79hu06xnigrjuq96s.webp" alt="Google Cloud Workload Identity Federation" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a &lt;strong&gt;Pool&lt;/strong&gt; (e.g., &lt;code&gt;unity-cicd-pool&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;strong&gt;Provider&lt;/strong&gt; inside that pool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Type&lt;/strong&gt;: OpenID Connect (OIDC)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Provider name&lt;/strong&gt;: &lt;code&gt;unity-cicd-provider&lt;/code&gt; (or any descriptive name - you will reference it in the workflow)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Issuer (URL)&lt;/strong&gt;: &lt;code&gt;https://token.actions.githubusercontent.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Audience&lt;/strong&gt;: &lt;code&gt;sts.amazonaws.com&lt;/code&gt; (Standard default for GitHub Actions).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Provider attributes&lt;/strong&gt; (attribute mapping):&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;google.subject&lt;/code&gt; = &lt;code&gt;assertion.sub&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;attribute.repository&lt;/code&gt; = &lt;code&gt;assertion.repository&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;attribute.ref&lt;/code&gt; = &lt;code&gt;assertion.ref&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;attribute.actor&lt;/code&gt; = &lt;code&gt;assertion.actor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Condition&lt;/strong&gt;: Use a minimal repo lock like &lt;code&gt;attribute.repository == "owner/repo"&lt;/code&gt;, for example &lt;code&gt;attribute.repository == "virtualmaker-net/proxima"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After creating the provider, go back to the page for your pool and at the top click &lt;strong&gt;Grant Access&lt;/strong&gt;. Select &lt;strong&gt;Grant access using service account impersonation&lt;/strong&gt; and choose your service account created in the previous section. Set the principals to &lt;code&gt;repository&lt;/code&gt; and the value to your &lt;code&gt;owner/repo&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5xi9osgudiku34mgbah.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe5xi9osgudiku34mgbah.webp" alt="Connect Workflow Identity Federation to Service Account" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Handling the Keystore
&lt;/h2&gt;

&lt;p&gt;Your Android Keystore is the identity of your app. If you lose it, you can't update your app. If it's stolen, someone else can update your app.&lt;/p&gt;

&lt;p&gt;For CI/CD, we have two common approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Base64 Encode&lt;/strong&gt;: Encode the &lt;code&gt;.keystore&lt;/code&gt; file to a base64 string and store it as a GitHub Secret.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Repo Storage&lt;/strong&gt;: Commit the keystore file to your repository (if private) or encrypted.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Method 1: Base64 Encoding (Recommended)
&lt;/h3&gt;

&lt;p&gt;This method keeps your keystore out of your repository entirely.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Encode&lt;/strong&gt;: Run one of the following commands to turn your file into a text string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; user.keystore &lt;span class="nt"&gt;-out&lt;/span&gt; user.keystore.base64.txt
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;IO.File&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ReadAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user.keystore"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Set-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user.keystore.base64.txt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Store&lt;/strong&gt;: Copy the contents of the text file and add it as a secret named &lt;code&gt;ANDROID_KEYSTORE_BASE64&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Method 2: Repo Storage
&lt;/h3&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; commit the keystore file to your repository (e.g. in &lt;code&gt;Assets/Keys/user.keystore&lt;/code&gt;), but this is risky. If your repo is ever compromised or made public, your signing key is exposed. If you choose this route, ensure your &lt;code&gt;.gitignore&lt;/code&gt; does not ignore &lt;code&gt;.keystore&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Regardless of the method, go to your GitHub Repository &amp;gt; &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Secrets and variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt; and add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ANDROID_KEYSTORE_PASSWORD&lt;/code&gt;: The password for your keystore.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANDROID_KEYALIAS_PASSWORD&lt;/code&gt;: The password for your key alias (often the same as the keystore, but not always).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Updating the Build Pipeline
&lt;/h2&gt;

&lt;p&gt;Now let's update our workflow to include the Android build, Google Cloud authentication, and Play Store upload steps. We'll be using the following actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-github-actions/auth" rel="noopener noreferrer"&gt;google-github-actions/auth&lt;/a&gt; - Authenticates with Google Cloud via Workload Identity Federation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/RageAgainstThePixel/upload-google-play-console" rel="noopener noreferrer"&gt;RageAgainstThePixel/upload-google-play-console&lt;/a&gt; - Uploads an AAB to the Google Play Console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create or update your &lt;code&gt;.github/workflows/unity-build.yml&lt;/code&gt; file with the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Google Play&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="c1"&gt;# Required for Workload Identity Federation&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Deploy to Google Play&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon-windows&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Checkout the repository&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;lfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup Unity with Android build support&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-setup@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;build-targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android&lt;/span&gt;

      &lt;span class="c1"&gt;# Activate Unity License&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/activate-unity-license@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Personal'&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.UNITY_EMAIL }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.UNITY_PASSWORD }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Add the Buildalon command line package to your Unity project&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add Build Pipeline Package&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install -g openupm-cli&lt;/span&gt;
          &lt;span class="s"&gt;openupm add com.virtualmaker.buildalon&lt;/span&gt;

      &lt;span class="c1"&gt;# Decode base64 keystore from GitHub Secrets&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Decode Keystore&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | openssl base64 -d -out ${{ github.workspace }}/user.keystore&lt;/span&gt;

      &lt;span class="c1"&gt;# Build the Android App Bundle (.aab)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Android AAB&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
            &lt;span class="s"&gt;-quit -batchmode&lt;/span&gt;
            &lt;span class="s"&gt;-executeMethod Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild&lt;/span&gt;
            &lt;span class="s"&gt;-keystorePath ${{ github.workspace }}/user.keystore&lt;/span&gt;
            &lt;span class="s"&gt;-keystorePass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"&lt;/span&gt;
            &lt;span class="s"&gt;-keyaliasName "my-key-alias"&lt;/span&gt;
            &lt;span class="s"&gt;-keyaliasPass "${{ secrets.ANDROID_KEYALIAS_PASSWORD }}"&lt;/span&gt;
            &lt;span class="s"&gt;-appBundle&lt;/span&gt;
          &lt;span class="na"&gt;log-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android-Build&lt;/span&gt;

      &lt;span class="c1"&gt;# Authenticate with Google Cloud using Workload Identity Federation&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# The service account email from Google Cloud&lt;/span&gt;
          &lt;span class="na"&gt;service_account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unity-cicd-uploader@my-project.iam.gserviceaccount.com&lt;/span&gt;
          &lt;span class="c1"&gt;# The full resource name of the Workload Identity Provider&lt;/span&gt;
          &lt;span class="na"&gt;workload_identity_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projects/123456789/locations/global/workloadIdentityPools/unity-cicd-pool/providers/unity-cicd-provider&lt;/span&gt;

      &lt;span class="c1"&gt;# Java is required by the Play Console upload tool&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temurin&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;21&lt;/span&gt;

      &lt;span class="c1"&gt;# Upload the AAB to the Google Play Internal Testing track&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RageAgainstThePixel/upload-google-play-console@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

          &lt;span class="c1"&gt;# Directory containing the .aab file&lt;/span&gt;
          &lt;span class="na"&gt;release-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}/Builds/Android&lt;/span&gt;

          &lt;span class="c1"&gt;# The track to upload to (internal, alpha, beta, production)&lt;/span&gt;
          &lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;

          &lt;span class="c1"&gt;# Draft status lets you review before rolling out&lt;/span&gt;
          &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;draft&lt;/span&gt;

          &lt;span class="c1"&gt;# Optional release notes&lt;/span&gt;
          &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"releaseNotes": {&lt;/span&gt;
                &lt;span class="s"&gt;"language": "en-US",&lt;/span&gt;
                &lt;span class="s"&gt;"text": "${{ env.RELEASE_NOTES }}"&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Steps Explained
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decode Keystore&lt;/strong&gt;: Converts the base64-encoded keystore secret back into a file for signing. If you checked in your keystore, you can skip this step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build Android AAB&lt;/strong&gt;: Runs the Unity build with keystore credentials and &lt;code&gt;-appBundle&lt;/code&gt; to produce an &lt;code&gt;.aab&lt;/code&gt; instead of an &lt;code&gt;.apk&lt;/code&gt;. Replace &lt;code&gt;my-key-alias&lt;/code&gt; with the alias you chose when creating your keystore in Unity (&lt;strong&gt;Project Settings &amp;gt; Player &amp;gt; Publishing Settings&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticate with Google Cloud&lt;/strong&gt;: Uses Workload Identity Federation to get a short-lived token. You need to replace two values:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;service_account&lt;/code&gt;: The email address from &lt;strong&gt;step 2.5&lt;/strong&gt; above (visible on the Service Account details page in Google Cloud Console under &lt;strong&gt;IAM &amp;amp; Admin &amp;gt; Service Accounts&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workload_identity_provider&lt;/code&gt;: The full resource name of the provider. You can find this in &lt;strong&gt;IAM &amp;amp; Admin &amp;gt; Workload Identity Federation&lt;/strong&gt;, click your pool, then click your provider, and copy the &lt;strong&gt;Default audience&lt;/strong&gt; or &lt;strong&gt;Resource name&lt;/strong&gt; value. It follows the format &lt;code&gt;projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID&lt;/code&gt;. Your &lt;strong&gt;Project Number&lt;/strong&gt; (not Project ID) is on the Google Cloud Console &lt;strong&gt;Dashboard&lt;/strong&gt; or &lt;strong&gt;Project Settings&lt;/strong&gt; page.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Upload to Play Console&lt;/strong&gt;: Pushes the &lt;code&gt;.aab&lt;/code&gt; to the &lt;strong&gt;internal&lt;/strong&gt; testing track as a draft, so your team can test immediately.&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: We are deploying to the &lt;code&gt;internal&lt;/code&gt; track. This is usually the best place for automated builds so your team can test them immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deploying to Internal Testing
&lt;/h2&gt;

&lt;p&gt;Once you push these changes, your workflow should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Build the Android App Bundle (&lt;code&gt;.aab&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; Authenticate with Google Cloud.&lt;/li&gt;
&lt;li&gt; Upload the &lt;code&gt;.aab&lt;/code&gt; to the Internal Testing track on the Play Console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the upload finishes, your testers in the Internal Testing email list will receive an update (or see it in the Play Store if they have it installed).&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Gotchas
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Android Version Code&lt;/strong&gt;: Google Play requires a unique integer &lt;code&gt;versionCode&lt;/code&gt; for every single upload. Ensure your build script increments this on every build (e.g., using &lt;code&gt;${{ github.run_number }}&lt;/code&gt;) in your Unity Android Build settings or build script.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Android Build Number&lt;/strong&gt;: Similar to &lt;code&gt;versionCode&lt;/code&gt;, the user-visible &lt;code&gt;bundleVersion&lt;/code&gt; (or &lt;strong&gt;Unity App Version&lt;/strong&gt;) should be updated to help testers identify which build they are on.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Play App Signing&lt;/strong&gt;: When you create your app in the console, you likely opted into &lt;strong&gt;Play App Signing&lt;/strong&gt;. This means Google manages the key used to sign APKs delivered to users. The keystore we configured above is your &lt;strong&gt;Upload Key&lt;/strong&gt;. If you lose it, you can contact Google support to reset it, whereas the final signing key is safely stored by Google.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;API Delays&lt;/strong&gt;: Sometimes the Google Play API takes a few minutes to process a new build.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Permissions&lt;/strong&gt;: If you get a 403 error, double-check that the Service Account email is added to the Google Play Console &lt;strong&gt;Users&lt;/strong&gt; with the correct permissions &lt;em&gt;and&lt;/em&gt; linked to the Google Cloud project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installing the Internal App and Inviting Testers
&lt;/h2&gt;

&lt;p&gt;After your first successful upload to the &lt;code&gt;internal&lt;/code&gt; track, you need to explicitly invite testers and share the opt-in link.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invite Internal Testers
&lt;/h3&gt;

&lt;p&gt;In the Google Play Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your app and go to &lt;strong&gt;Testing &amp;gt; Internal testing&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;Testers&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Create an email list (or select an existing one) and add tester emails.&lt;/li&gt;
&lt;li&gt;Save the list and make sure it is attached to your internal test track.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Opt-in URL&lt;/strong&gt; from the internal testing page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Share that opt-in URL with your testers. They must accept the invite using the same Google account you added to the tester list.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Testers Install the Internal Version
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Tester opens the opt-in URL and taps &lt;strong&gt;Become a tester&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tester taps the Play Store link shown on that page.&lt;/li&gt;
&lt;li&gt;Tester installs the app (or updates it if already installed).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the app is already installed from another source (for example, a local APK), the tester may need to uninstall it first if the signing key or package signature does not match the Play build.&lt;/p&gt;

&lt;p&gt;For later releases, once users are opted in, they usually receive updates through the Play Store automatically (or via manual update if auto-update is disabled).&lt;/p&gt;

&lt;h2&gt;
  
  
  About Buildalon
&lt;/h2&gt;

&lt;p&gt;Buildalon provides verified GitHub Actions and dedicated build infrastructure to streamline Unity development. &lt;a href="https://www.buildalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Register for Buildalon&lt;/a&gt; to get support with your Unity build automation needs.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>playstore</category>
      <category>gamedev</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Automating Unity Builds for Meta Quest</title>
      <dc:creator>Alon Farchy</dc:creator>
      <pubDate>Mon, 26 Jan 2026 16:33:22 +0000</pubDate>
      <link>https://dev.to/virtualmaker/automating-unity-builds-for-meta-quest-2lpf</link>
      <guid>https://dev.to/virtualmaker/automating-unity-builds-for-meta-quest-2lpf</guid>
      <description>&lt;p&gt;Developing for VR on the Meta Quest involves a lot of iteration. Since many device capabilities only work on the headset, you'll find yourself making frequent builds to sideload. Distributing those builds to play-testers manually can consume much of your time. As your app and team grow, you start wishing this process was automated.&lt;/p&gt;

&lt;p&gt;In this guide, we'll set up a completely automated pipeline that takes your code from GitHub, builds it in the cloud, and delivers it directly to a release channel on the Meta Quest Store.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: This guide assumes you are already familiar with setting up basic Unity builds with GitHub Actions. If you're just starting out, check out my guide on &lt;a href="https://dev.to/virtualmaker/automating-unity-builds-with-github-actions-1inf"&gt;Automating Unity Builds with GitHub Actions&lt;/a&gt; first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By the end of this tutorial, you will have a GitHub Actions workflow that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Triggers whenever you push to your &lt;code&gt;main&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;Builds your Unity project for Android (Meta Quest).&lt;/li&gt;
&lt;li&gt;Uploads the resulting APK directly to a specific Release Channel (e.g., Alpha) on the Meta Quest Store.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Setting up the Meta Developer Portal&lt;/li&gt;
&lt;li&gt;Configure GitHub Secrets for Meta&lt;/li&gt;
&lt;li&gt;Configure GitHub Secrets for Keystores and Signing&lt;/li&gt;
&lt;li&gt;Update Your Build Workflow&lt;/li&gt;
&lt;li&gt;Installing the Build on Your Headset&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setting up the Meta Developer Portal
&lt;/h2&gt;

&lt;p&gt;Before we touch any YAML configuration, we need to prepare our application on the &lt;a href="https://developer.oculus.com/manage/" rel="noopener noreferrer"&gt;Meta Developer Dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Your App
&lt;/h3&gt;

&lt;p&gt;If you haven't created an app yet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to the Developer Dashboard.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2cqi0rruyxxd2nq6r3x6.png" alt="Meta Developer Dashboard - Create New App" width="800" height="400"&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create New App&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give it a name and click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Get Your Credentials
&lt;/h3&gt;

&lt;p&gt;To allow GitHub Actions to upload builds on your behalf, you need two pieces of information: the &lt;strong&gt;App ID&lt;/strong&gt; and the &lt;strong&gt;App Secret&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;Development&lt;/strong&gt; &amp;gt; &lt;strong&gt;API&lt;/strong&gt; tab in your app's dashboard.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft9b5nk3rp5taklpbuxx6.png" alt="Meta Developer Dashboard - API" width="800" height="496"&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;App ID&lt;/strong&gt;. You'll need this for your workflow file.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;App Secret&lt;/strong&gt;. &lt;strong&gt;Do not commit this to your repository!&lt;/strong&gt; We will save this as a secret in GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;TIP: On some accounts, you may need to verify your account or complete the Data Use Checkup before you can access full API functionality. If your upload fails later with permission errors, double-check that your developer account status is in good standing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxeps8jr5eq90xfzvkcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxeps8jr5eq90xfzvkcc.png" alt="Meta Developer Dashboard - Data Use Checkup Notice" width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a Release Channel
&lt;/h3&gt;

&lt;p&gt;Release channels let you to distribute different versions of your app to different users. For example, you might have a "Live" channel for the public and an "Alpha" channel for internal testing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Distribution&lt;/strong&gt; &amp;gt; &lt;strong&gt;Release Channels&lt;/strong&gt;.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjphnhjt8corcpp9xoeys.png" alt="Meta Developer Dashboard - Release Channels" width="800" height="446"&gt;
&lt;/li&gt;
&lt;li&gt;You likely already have &lt;code&gt;LIVE&lt;/code&gt;, &lt;code&gt;ALPHA&lt;/code&gt;, &lt;code&gt;BETA&lt;/code&gt;, and &lt;code&gt;RC&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can use one of these (e.g., &lt;code&gt;ALPHA&lt;/code&gt;) or create a new one specifically for automated builds, like &lt;code&gt;NIGHTLY&lt;/code&gt; or &lt;code&gt;ACTIONS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Note the exact name of the channel you want to target.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configure GitHub Secrets for Meta
&lt;/h2&gt;

&lt;p&gt;We never want to hardcode our App Secret into our workflow files where anyone could see them. Instead, we use GitHub Secrets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to your GitHub repository.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Secrets and variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt;.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83vu3ptldh7bxz4qgwcr.png" alt="GitHub - Action Secrets" width="800" height="629"&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;New repository secret&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create a secret named &lt;code&gt;META_APP_SECRET&lt;/code&gt; and paste the value you copied from the Meta Developer Dashboard.&lt;/li&gt;
&lt;li&gt;(Optional) You can also store your App ID as &lt;code&gt;META_APP_ID&lt;/code&gt;, though it is generally considered public information.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Configure GitHub Secrets for Keystores and Signing
&lt;/h2&gt;

&lt;p&gt;One critical detail for Android builds is code signing. The Meta Quest Store requires your APKs to be signed with a production keystore, even for the Alpha channel. If you try to upload a development build signed with a default debug keystore, the action will successfully build, but the Meta server will reject the upload during validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Keystore?
&lt;/h3&gt;

&lt;p&gt;A keystore is a binary file that acts as a digital certificate for your app. It proves that the update comes from the same developer as the original app. If you lose your keystore, you lose the ability to update your app on the store—forever. So keep it safe!&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Keystore
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open your Unity project.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Project Settings &amp;gt; Player &amp;gt; Publishing Settings&lt;/strong&gt;.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0v7vsascru9r2jbn757r.png" alt="Unity - Keystore Management" width="800" height="446"&gt;
&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Keystore Manager&lt;/strong&gt;, create a new keystore.&lt;/li&gt;
&lt;li&gt;Choose a location inside your project folder.&lt;/li&gt;
&lt;li&gt;Set a &lt;strong&gt;Keystore Password&lt;/strong&gt;, create a new Key Alias, and set a &lt;strong&gt;Key Password&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Go to your GitHub repository &lt;strong&gt;Settings &amp;gt; Secrets and variables &amp;gt; Actions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Create two new secrets for your passwords: &lt;code&gt;ANDROID_KEYSTORE_PASS&lt;/code&gt; and &lt;code&gt;ANDROID_KEYALIAS_PASS&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;Buildalon&lt;/code&gt; build script we are using is designed to pick up these secrets from the command line arguments and apply them to the keystore found in your project settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keystore Security
&lt;/h3&gt;

&lt;p&gt;For better security, you can encode your keystore in base64 and store it as a GitHub Secret instead of committing the file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Encode the keystore in base64.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On macOS/Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; user.keystore &lt;span class="nt"&gt;-out&lt;/span&gt; user.keystore.base64.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows (PowerShell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;IO.File&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ReadAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"user.keystore"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Set-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user.keystore.base64.txt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store:&lt;/strong&gt; Add it as a secret named &lt;code&gt;ANDROID_KEYSTORE_BASE64&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode:&lt;/strong&gt; Add a step in your workflow (below) to decode it back to a file using &lt;code&gt;openssl base64 -d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build:&lt;/strong&gt; Pass &lt;code&gt;-keystorePath user.keystore&lt;/code&gt; in your &lt;code&gt;unity-action&lt;/code&gt; build arguments.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Update Your Build Workflow
&lt;/h2&gt;

&lt;p&gt;Now, let's look at the workflow file. We'll build upon the standard Buildalon Android workflow but add a crucial final step: the upload.&lt;/p&gt;

&lt;p&gt;We'll be using the following actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/buildalon/setup-ovr-platform-util" rel="noopener noreferrer"&gt;buildalon/setup-ovr-platform-util&lt;/a&gt; - Installs OVR Platform Tool.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/buildalon/upload-meta-quest-build" rel="noopener noreferrer"&gt;buildalon/upload-meta-quest-build&lt;/a&gt; - Invokes OVR Platform Tool to upload your build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create or update your &lt;code&gt;.github/workflows/quest-build.yml&lt;/code&gt; file with the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Quest&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build &amp;amp; Deploy to Quest&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon-windows&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Checkout the repository&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;lfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup Unity&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-setup@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;build-targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android&lt;/span&gt;

      &lt;span class="c1"&gt;# Activate Unity License&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/activate-unity-license@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Personal'&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.UNITY_EMAIL }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.UNITY_PASSWORD }}&lt;/span&gt;

      &lt;span class="c1"&gt;# Temporarily add the Buildalon command line package to your Unity project&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add Build Pipeline Package&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install -g openupm-cli&lt;/span&gt;
          &lt;span class="s"&gt;openupm add com.virtualmaker.buildalon&lt;/span&gt;

      &lt;span class="c1"&gt;# Optional: Decode base64 keystore from GitHub Secrets&lt;/span&gt;
      &lt;span class="c1"&gt;# - name: Decode Keystore&lt;/span&gt;
      &lt;span class="c1"&gt;#   run: |&lt;/span&gt;
      &lt;span class="c1"&gt;#     echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | openssl base64 -d -out ${{ env.UNITY_PROJECT_PATH }}/user.keystore&lt;/span&gt;

      &lt;span class="c1"&gt;# Build the Project&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Android APK&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Android&lt;/span&gt;
          &lt;span class="c1"&gt;# Optional: add a previous step to decode a base64 keystore, and add: -keystorePath ${{ env.UNITY_PROJECT_PATH }}/user.keystore&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
            &lt;span class="s"&gt;-quit -batchmode&lt;/span&gt;
            &lt;span class="s"&gt;-executeMethod Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild&lt;/span&gt;
            &lt;span class="s"&gt;-keyaliasPass "${{ secrets.ANDROID_KEYALIAS_PASS }}"&lt;/span&gt;
            &lt;span class="s"&gt;-keystorePass "${{ secrets.ANDROID_KEYSTORE_PASS }}"&lt;/span&gt;
          &lt;span class="na"&gt;log-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Quest-Build&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup OVR Platform Util&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/setup-ovr-platform-util@v1&lt;/span&gt;

      &lt;span class="c1"&gt;# Upload to Meta Quest Store&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/upload-meta-quest-build@v1&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload to Meta Quest&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Your App ID (can be hardcoded or a secret)&lt;/span&gt;
          &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;123456789012345'&lt;/span&gt;

          &lt;span class="c1"&gt;# Your App Secret&lt;/span&gt;
          &lt;span class="na"&gt;appSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.META_APP_SECRET }}&lt;/span&gt;

          &lt;span class="c1"&gt;# The path to your built APK.&lt;/span&gt;
          &lt;span class="na"&gt;buildDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}/Builds/Android&lt;/span&gt;

          &lt;span class="c1"&gt;# The channel to upload to (ALPHA, BETA, LIVE, etc.)&lt;/span&gt;
          &lt;span class="na"&gt;releaseChannel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ALPHA'&lt;/span&gt;

          &lt;span class="c1"&gt;# Optional, uploads the build even if there are validation errors.&lt;/span&gt;
          &lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Changes:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Set the build platform to Android.&lt;/li&gt;
&lt;li&gt;Pass the keystore credentials to the build step.&lt;/li&gt;
&lt;li&gt;Install the OVR Platform Tools&lt;/li&gt;
&lt;li&gt;Upload the built APK.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The magic happens in the &lt;code&gt;buildalon/upload-meta-quest-build&lt;/code&gt; step. This action wraps the Oculus Platform Command Line Utility, handling the authentication and upload process for you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;appId&lt;/code&gt;: Identifies which application specifically you are updating.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;appSecret&lt;/code&gt;: Authenticates the upload request.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;buildDir&lt;/code&gt;: Tells the uploader where to find your build. Note that &lt;code&gt;StartCommandLineBuild&lt;/code&gt; by default outputs builds to the &lt;code&gt;Builds/{Platform}&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;releaseChannel&lt;/code&gt;: Ensures the build lands in the right place so your testers get the update without affecting live users.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installing the Build on Your Headset
&lt;/h2&gt;

&lt;p&gt;Once your build succeeds, it will be uploaded to the &lt;strong&gt;ALPHA&lt;/strong&gt; channel.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Invite Users:&lt;/strong&gt; In the Meta Developer Dashboard, go to &lt;strong&gt;Distribution &amp;gt; Users&lt;/strong&gt;. Add yourself and your teammates to the ALPHA channel using their Meta account emails.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Accept the Invite:&lt;/strong&gt; Each person will receive an email invitation to test the app. They must accept this invite.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Find the App:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Put on your Quest headset.&lt;/li&gt;
&lt;li&gt;  Navigate to your &lt;strong&gt;App Library&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  Look for the app in the &lt;strong&gt;All&lt;/strong&gt; or &lt;strong&gt;Not Installed&lt;/strong&gt; category. It may also appear in the &lt;strong&gt;My Preview Apps&lt;/strong&gt; section of the Store.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Install&lt;/strong&gt; the application.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Confirm the Channel:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Navigate to the app details in your Library or on the Store page.&lt;/li&gt;
&lt;li&gt;  Click the version number to verify or switch the release channel to "ALPHA" (or your chosen channel).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once installed, future updates from your GitHub Actions pipeline will appear as regular app updates on the headset!&lt;/p&gt;

&lt;p&gt;Mission complete!&lt;/p&gt;

&lt;h2&gt;
  
  
  About Buildalon
&lt;/h2&gt;

&lt;p&gt;Buildalon provides verified GitHub Actions and dedicated build infrastructure to streamline Unity development. &lt;a href="https://www.buildalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Register for Buildalon&lt;/a&gt; to get support with your Unity build automation needs.&lt;/p&gt;

</description>
      <category>metaquest</category>
      <category>unity3d</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Automating Unity Builds to iOS, macOS, and visionOS</title>
      <dc:creator>Alon Farchy</dc:creator>
      <pubDate>Fri, 16 Jan 2026 12:35:30 +0000</pubDate>
      <link>https://dev.to/virtualmaker/automating-unity-builds-to-ios-macos-and-visionos-14km</link>
      <guid>https://dev.to/virtualmaker/automating-unity-builds-to-ios-macos-and-visionos-14km</guid>
      <description>&lt;h1&gt;
  
  
  Automating Unity Builds to iOS, macOS, and visionOS
&lt;/h1&gt;

&lt;p&gt;When deploying a Unity game to Apple platforms, you can easily get bogged down with provisioning profiles, signing certificates, and Xcode configuration. It's a time complex consuming process, and manually repeating it for every build is going to eat up your time and slow your team down.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through how to use &lt;strong&gt;GitHub Actions&lt;/strong&gt; and &lt;strong&gt;Buildalon&lt;/strong&gt; to automate your builds for &lt;strong&gt;iOS&lt;/strong&gt;, &lt;strong&gt;macOS&lt;/strong&gt;, and &lt;strong&gt;visionOS&lt;/strong&gt;, covering everything from setting up your Apple Developer account to creating a workflow that deploys straight to your testers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Setup Overview&lt;/li&gt;
&lt;li&gt;Setting up App Store Connect&lt;/li&gt;
&lt;li&gt;Generating a Build for Sideloading&lt;/li&gt;
&lt;li&gt;Deploying to TestFlight&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we dive into the automation, there are a few things you'll need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Apple Developer Account&lt;/strong&gt;: You need an active membership to deploy to Apple devices or the App Store. &lt;a href="https://developer.apple.com/programs/enroll/" rel="noopener noreferrer"&gt;Enroll in the Apple Developer Program&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Working Unity Build&lt;/strong&gt;: You should already have a basic automated build pipeline. If not, follow our &lt;a href="https://dev.to/virtualmaker/automating-unity-builds-with-github-actions-1inf"&gt;Automating Unity Builds with GitHub Actions&lt;/a&gt; guide first.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup Overview
&lt;/h2&gt;

&lt;p&gt;Automating Xcode builds involves connecting several services with your GitHub workflow. At a high level we need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Registration&lt;/strong&gt;: We need to register the App ID and create an app record in App Store Connect so Apple knows what we are building.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Authentication&lt;/strong&gt;: We will create an &lt;strong&gt;App Store Connect API Key&lt;/strong&gt; to allow GitHub Actions to act on our behalf without 2FA issues.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Update the Build Pipeline&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Unity Build&lt;/strong&gt;: Unity runs in batch mode to export an Xcode project. It's important to set the &lt;code&gt;bundleVersion&lt;/code&gt; and &lt;code&gt;buildNumber&lt;/code&gt; here to track our releases.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Xcode Build&lt;/strong&gt;: We use &lt;code&gt;xcodebuild&lt;/code&gt; (wrapped in a GitHub Action) to compile, sign, and archive the app into an &lt;code&gt;.ipa&lt;/code&gt; (or &lt;code&gt;.app&lt;/code&gt;/&lt;code&gt;.pkg&lt;/code&gt; for macOS) file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Distribution&lt;/strong&gt;: Finally, we either upload the build to TestFlight for beta testing or keep it as an artifact for manual sideloading.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key to making this easy is the &lt;a href="https://github.com/buildalon/unity-xcode-builder" rel="noopener noreferrer"&gt;buildalon/unity-xcode-builder&lt;/a&gt; action, which handles the complex Xcode build and upload steps for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the App on Apple's Portal
&lt;/h2&gt;

&lt;p&gt;Before automation can take over, we need to do a little manual setup to tell Apple about our app. This is a one-time process for each new project.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Log in to &lt;a href="https://appstoreconnect.apple.com/" rel="noopener noreferrer"&gt;App Store Connect&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Go to &lt;strong&gt;My Apps&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click the blue &lt;strong&gt;(+)&lt;/strong&gt; button &amp;gt; &lt;strong&gt;New App&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Select your platform (&lt;strong&gt;iOS&lt;/strong&gt;, &lt;strong&gt;macOS&lt;/strong&gt;, or &lt;strong&gt;visionOS&lt;/strong&gt;) and give it a &lt;strong&gt;Name&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; In the &lt;strong&gt;Bundle ID&lt;/strong&gt; section, you can choose an existing identifier or create a new one. Ensure this matches the Bundle Identifier in your Unity project (e.g., &lt;code&gt;com.mycompany.mygame&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; Enter a &lt;strong&gt;SKU&lt;/strong&gt; (a unique tracking ID for your own use) and click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setting up App Store Connect Authentication
&lt;/h2&gt;

&lt;p&gt;For GitHub Actions to upload builds on your behalf, it needs a secure way to authenticate with Apple. The best way to do this is with an &lt;strong&gt;App Store Connect API Key&lt;/strong&gt;, which avoids the headaches of 2FA in CI environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Generate an API Key
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Head over to &lt;a href="https://appstoreconnect.apple.com/" rel="noopener noreferrer"&gt;App Store Connect&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt; Go to &lt;strong&gt;Users and Access&lt;/strong&gt; &amp;gt; &lt;strong&gt;Integrations&lt;/strong&gt; &amp;gt; &lt;strong&gt;Team Keys&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click the &lt;strong&gt;+&lt;/strong&gt; button to generate a new key.&lt;/li&gt;
&lt;li&gt; Give it a name like "GitHub Actions" and assign the &lt;strong&gt;App Manager&lt;/strong&gt; role.&lt;/li&gt;
&lt;li&gt; Download the &lt;code&gt;.p8&lt;/code&gt; file. &lt;strong&gt;Keep this safe!&lt;/strong&gt; You can only download it once.&lt;/li&gt;
&lt;li&gt; Take note of the &lt;strong&gt;Key ID&lt;/strong&gt; and your &lt;strong&gt;Issuer ID&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. Base64 Encode the Key
&lt;/h3&gt;

&lt;p&gt;GitHub Secrets can sometimes struggle with the newlines in the &lt;code&gt;.p8&lt;/code&gt; file. A robust way to handle this is to Base64 encode the file content.&lt;/p&gt;

&lt;p&gt;Run this command in your terminal (macOS/Linux):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; AuthKey_XXXXXXXXXX.p8 &lt;span class="nt"&gt;-out&lt;/span&gt; AuthKey_Base64.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows (PowerShell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;IO.File&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ReadAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"AuthKey_XXXXXXXXXX.p8"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Set-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AuthKey_Base64.txt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the contents of the generated text file.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Add Secrets to GitHub
&lt;/h3&gt;

&lt;p&gt;Go to your repository on GitHub, navigate to &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Secrets and variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt;, and add the following secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;APP_STORE_CONNECT_KEY&lt;/code&gt;: The Base64 encoded string from the previous step.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;APP_STORE_CONNECT_KEY_ID&lt;/code&gt;: The Key ID (e.g., &lt;code&gt;D383SF739&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;APP_STORE_CONNECT_ISSUER_ID&lt;/code&gt;: The Issuer UUID (e.g., &lt;code&gt;6053b7fe...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;APPLE_TEAM_ID&lt;/code&gt;: Your Team ID (found in &lt;a href="https://developer.apple.com/account" rel="noopener noreferrer"&gt;Apple Developer Portal&lt;/a&gt; under Membership).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you notice your build failing with authentication errors, double-check that you copied the stored secret correctly and that it doesn't contain any extra whitespace.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Generating a Build for Sideloading
&lt;/h2&gt;

&lt;p&gt;Before messing with TestFlight, it's often easier to just get an installable build (an &lt;code&gt;.ipa&lt;/code&gt; for iOS/visionOS, or &lt;code&gt;.app&lt;/code&gt;/&lt;code&gt;.pkg&lt;/code&gt; for macOS) that you can install directly. This is great for rapid iteration or testing on a few specific devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Workflow
&lt;/h3&gt;

&lt;p&gt;Now let's update your build workflow to add steps for building and signing your app. If you don't have a workflow yet, see our &lt;a href="https://dev.to/virtualmaker/automating-unity-builds-with-github-actions-1inf"&gt;Automating Unity Builds&lt;/a&gt; article to get started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing a Runner:&lt;/strong&gt;&lt;br&gt;
To build for Apple platforms, you &lt;strong&gt;must&lt;/strong&gt; use a macOS runner. You have two main options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;runs-on: macos-latest&lt;/code&gt;: GitHub's hosted runners. They are convenient for one-off builds, but can be slow and expensive if you're building frequently.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;runs-on: buildalon-macos&lt;/code&gt;: &lt;a href="https://www.buildalon.com" rel="noopener noreferrer"&gt;Buildalon's runners&lt;/a&gt;. These will cache your Unity builds, often completing much faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, we'll use &lt;code&gt;buildalon-macos&lt;/code&gt; for best performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Note on Build Numbers:&lt;/strong&gt;&lt;br&gt;
Unity projects use the build number set in the editor (&lt;code&gt;ProjectSettings.asset&lt;/code&gt;) by default. In a CI/CD pipeline, it's best practice to override this so every build has a unique identifier. In the example below, we'll use &lt;code&gt;${{ github.run_number }}&lt;/code&gt; as the build number, while keeping the version string (e.g. 1.0) defined in your project settings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-iOS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build iOS&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon-macos&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;lfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup Unity, License, and Build the Project&lt;/span&gt;
      &lt;span class="c1"&gt;# See our "Automating Unity Builds" article for details on these steps&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-setup@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Use 'iOS', 'Standalone', or 'VisionOS'&lt;/span&gt;
          &lt;span class="na"&gt;build-targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;iOS&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/activate-unity-license@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Personal'&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_EMAIL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_PASSWORD&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Unity Project&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Use 'iOS', 'StandaloneOSX', or 'VisionOS'&lt;/span&gt;
          &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;iOS&lt;/span&gt;
          &lt;span class="c1"&gt;# Use run_number as the build number (e.g. 1.0.42)&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-quit -batchmode -executeMethod Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild -buildNumber ${{ github.run_number }}&lt;/span&gt;
          &lt;span class="na"&gt;log-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;iOS-Build&lt;/span&gt;

      &lt;span class="c1"&gt;# Build the XCode project and sign it&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-xcode-builder@v1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xcode-build&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workspace }}/Builds/iOS/**/*.xcodeproj&lt;/span&gt;
          &lt;span class="c1"&gt;# 'development' creates a build signed for specific devices&lt;/span&gt;
          &lt;span class="na"&gt;export-option&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
          &lt;span class="na"&gt;team-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APPLE_TEAM_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;app-store-connect-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_STORE_CONNECT_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;app-store-connect-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_STORE_CONNECT_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;app-store-connect-issuer-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;iOS-Build&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.xcode-build.outputs.executable }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Grab the Artifact
&lt;/h3&gt;

&lt;p&gt;Once the workflow finishes successfully, you can find your build file waiting for you in the &lt;strong&gt;Summary&lt;/strong&gt; page of the workflow run.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
    &lt;strong&gt;The Catch:&lt;/strong&gt; To install a development build, the destination device's &lt;strong&gt;UDID&lt;/strong&gt; must be registered in the Apple Developer Portal &lt;em&gt;before&lt;/em&gt; the build runs. If you get a new device, you'll need to add it to the portal and trigger a fresh build to update the provisioning profile.&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Installing the Build
&lt;/h3&gt;

&lt;p&gt;Since this is a raw development build, you can't just tap a link to install it. You'll need to use one of the following methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;iOS/visionOS:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Finder (macOS):&lt;/strong&gt; Connect your device via USB. In Finder, select your device from the sidebar and drag the &lt;code&gt;.ipa&lt;/code&gt; onto the general information window.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;macOS:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Simply download and unzip the artifact, then run the &lt;code&gt;.app&lt;/code&gt; or installer. Note that you may need to right-click and select "Open" to bypass security warnings if it's not notarized yet.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploying to TestFlight
&lt;/h2&gt;

&lt;p&gt;Sideloading is great for quick tests, but for beta testing with your team (or the world), &lt;strong&gt;TestFlight&lt;/strong&gt; is the way to go. This works for iOS, tvOS, visionOS, and macOS apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a Testing Group
&lt;/h3&gt;

&lt;p&gt;First, organize your testers in App Store Connect.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Navigate to &lt;strong&gt;App Store Connect&lt;/strong&gt; &amp;gt; &lt;strong&gt;My Apps&lt;/strong&gt; &amp;gt; Select your App &amp;gt; &lt;strong&gt;TestFlight&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Under &lt;strong&gt;Internal Testing&lt;/strong&gt;, click the &lt;strong&gt;(+)&lt;/strong&gt; button to create a group (e.g., "Internal Testers").&lt;/li&gt;
&lt;li&gt; Add your team members to this group.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. Update the Workflow
&lt;/h3&gt;

&lt;p&gt;Now we just need to tweak our workflow to tell it to upload to App Store Connect instead of just signing for development. Change the &lt;code&gt;export-option&lt;/code&gt; to &lt;code&gt;app-store-connect&lt;/code&gt; and specify your test group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-xcode-builder@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;project-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workspace }}/Builds/iOS/**/*.xcodeproj&lt;/span&gt;
          &lt;span class="na"&gt;export-option&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-store-connect&lt;/span&gt;
          &lt;span class="c1"&gt;# ... credentials ...&lt;/span&gt;
          &lt;span class="c1"&gt;# Automatically invite this group when the build processes&lt;/span&gt;
          &lt;span class="na"&gt;test-groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Internal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Testers"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the build finishes, Buildalon will upload the archive. Apple usually takes a few minutes to process it. Once that's done, your "Internal Testers" will get an email, and they can start testing immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Download &amp;amp; Play
&lt;/h3&gt;

&lt;p&gt;Your testers just need to grab the &lt;strong&gt;TestFlight&lt;/strong&gt; app from the App Store. Once they accept the invite, your game will appear there, ready to install.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Want to tell your testers what's new? Add the &lt;code&gt;release-notes&lt;/code&gt; parameter to the &lt;code&gt;buildalon/unity-xcode-builder&lt;/code&gt; step. You can even pass &lt;code&gt;${{ github.event.head_commit.message }}&lt;/code&gt; to automatically use your commit message!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  About Buildalon
&lt;/h2&gt;

&lt;p&gt;Buildalon provides verified GitHub Actions and dedicated build infrastructure to streamline Unity development. &lt;a href="https://www.buildalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Register for Buildalon&lt;/a&gt; to get support with your Unity build automation needs.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>githubactions</category>
      <category>ios</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Unit Testing for Unity Developers</title>
      <dc:creator>Alon Farchy</dc:creator>
      <pubDate>Wed, 22 Jan 2025 16:04:02 +0000</pubDate>
      <link>https://dev.to/virtualmaker/unit-testing-for-unity-developers-52lp</link>
      <guid>https://dev.to/virtualmaker/unit-testing-for-unity-developers-52lp</guid>
      <description>&lt;p&gt;Let's face it Unity developers — you write buggy code. &lt;strong&gt;I write buggy code&lt;/strong&gt;. &lt;strong&gt;&lt;em&gt;AI writes buggy code&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Many software engineers consider unit testing as the key to catching bugs early and preventing regressions. But do they work for Unity developers?&lt;/p&gt;

&lt;p&gt;In this article, I'll share with you how we do testing at Virtual Maker. We'll learn the difference between unit tests, integration tests, and end-to-end tests, and why I think you should avoid writing the latter.&lt;/p&gt;

&lt;p&gt;Then, we'll dive into some code and learn how to write tests using the NUnit framework in Unity. To top it off, we'll learn how to run tests from the command line and using GitHub Actions to truly automate the testing process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Unit Testing at Virtual Maker&lt;/li&gt;
&lt;li&gt;Types of Tests&lt;/li&gt;
&lt;li&gt;Writing Tests in Unity using NUnit&lt;/li&gt;
&lt;li&gt;Edit Mode Tests&lt;/li&gt;
&lt;li&gt;Play Mode Tests&lt;/li&gt;
&lt;li&gt;Run Tests from the Command Line&lt;/li&gt;
&lt;li&gt;Running Tests in Automation using GitHub Actions&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Unit Testing at Virtual Maker
&lt;/h2&gt;

&lt;p&gt;At Virtual Maker, we use unit testing to extensively test our Unity plugins and ensure they work in a variety of scenarios.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://www.flexalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Flexalon 3D Layouts&lt;/a&gt;, we test each layout component with different configurations and edge cases. Similarly, in &lt;a href="https://www.unityproxima.com?utm_source=devto" rel="noopener noreferrer"&gt;Proxima Inspector&lt;/a&gt;, we test the protocol between Unity and the browser, to ensure that the right data is sent for each type of component and gameObject.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/xYAgaOcm8c8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;On top of ensuring that all edge cases work correctly, unit tests help us to prevent regressions. Whenever we fix bugs or add new features, we invariably introduce some new bugs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faeli037k534nzjl6e4bl.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faeli037k534nzjl6e4bl.jpeg" alt="Fixing bugs can cause new bugs" width="639" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To catch these, we set up our tests to run automatically whenever we make a pull request in GitHub, so any of these bugs are caught well before a change makes it in into a release.&lt;/p&gt;

&lt;p&gt;But not all tests are created equal. In my past job at Microsoft, we spent a heck of a lot of time writing tests that didn't catch &lt;em&gt;any&lt;/em&gt; bugs. Worse, we wrote tests that were so brittle that we spent more time investigating issues and fixing &lt;em&gt;the tests&lt;/em&gt; than actually working on the product.&lt;/p&gt;

&lt;p&gt;So what makes one test good and another test bad?&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Tests
&lt;/h2&gt;

&lt;p&gt;Often, what separates a good test from a bad test is a matter of &lt;strong&gt;scope&lt;/strong&gt;. Ask yourself the question: how many components need to work correctly for a test to pass?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit Tests&lt;/strong&gt;: Test a single method or class in isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Tests&lt;/strong&gt;: Test the multiple components work together correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end Tests&lt;/strong&gt;: Test that game features work as expected from as close to a player's perspective as possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Unit Testing
&lt;/h3&gt;

&lt;p&gt;Advocates for &lt;strong&gt;unit testing&lt;/strong&gt; argue that tests should be as small as possible — test a single method or class in isolation. These tests are quick to run and easy to debug. For complex functions, especially math functions, they can also provide a lot of value.&lt;/p&gt;

&lt;p&gt;For example, Proxima Inspector has a &lt;code&gt;CircularList&lt;/code&gt; class that stores Unity logs so that you can see past logs after connecting the inspector. This class was tricky to get right, so we write some unit tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnumerableWrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CircularList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Constructs a circular list with a size of 2&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemsAdded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test checks that the &lt;code&gt;CircularList&lt;/code&gt; class wraps around when it reaches its capacity. It adds three items to the list, then checks that the list only contains the last two items.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integration Testing
&lt;/h3&gt;

&lt;p&gt;Scoped unit testing is great for checking the behavior of complex functions and classes. But often, it's not enough. In Flexalon, most of the difficult bugs come from the interactions between multiple Flexalon components.&lt;/p&gt;

&lt;p&gt;In this case, we should think of how to test the Flexalon package as an isolated component.&lt;/p&gt;

&lt;p&gt;How can we test this scenario? The test needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a scene.&lt;/li&gt;
&lt;li&gt;Add some gameObjects with Flexalon components.&lt;/li&gt;
&lt;li&gt;Have Flexalon run its layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note 1: You can can still use a "unit testing framework" to write integration tests, or even end-to-end tests. Don't look at me, I don't make the rules.&lt;/p&gt;

&lt;p&gt;Note 2: "Integration testing" can also refer to the integration between modules, processes, or even distributed computers. Here, I'm using the term to refer to the scope of the test as bigger than a single class or method. Think of testing a whole package or library.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's an example of a real test from Flexalon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FillChild&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Create and configure a Flexible Layout&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateFlex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;flexObj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FlexalonObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;flexObj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Add a child to the layout&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateCube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FlexalonObject&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WidthOfParent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Add another child to the layout&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateCube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Update the layout&lt;/span&gt;
    &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Check the results&lt;/span&gt;
    &lt;span class="nf"&gt;AssertTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;AssertTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;AssertTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since there are many such tests, we wrote helper methods for common functionality: create Flexalon components, update the layout, and check the results.&lt;/p&gt;

&lt;p&gt;Importantly, these tests run in &lt;strong&gt;edit mode&lt;/strong&gt;. We don't need to play the game to run these tests, and we don't have to perform any asynchronous actions. &lt;strong&gt;Flexalon was designed this way&lt;/strong&gt; so that it would be easy to test. By calling &lt;code&gt;Update()&lt;/code&gt;, we force Flexalon to immediately process all layout updates, even though it would normally wait for the next frame.&lt;/p&gt;

&lt;p&gt;This is key: write your components so they are easy to test in isolation. If your tests are not fast and reliable, they will quickly lose their value. This brings us to...&lt;/p&gt;

&lt;h3&gt;
  
  
  End-to-end Testing
&lt;/h3&gt;

&lt;p&gt;In end-to-end testing, we try to test the product as a user would use it. Typically, this involves setting up some input simulation so that the test can act as a player, play the game, and then check the results.&lt;/p&gt;

&lt;p&gt;With end-to-end tests, you can be sure that the product is really working as expected. By bother testing every edge of every component in isolation when you can just test the real player experience?&lt;/p&gt;

&lt;p&gt;Or, so the thinking goes.&lt;/p&gt;

&lt;p&gt;In reality, end-to-end are often &lt;strong&gt;slow&lt;/strong&gt;, &lt;strong&gt;brittle&lt;/strong&gt;, and &lt;strong&gt;hard to maintain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's use a simple example. Suppose you want to test that the player can change the volume. First, the player has to open the main menu, then drag the volume slider. What are the problems?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Slow:&lt;/strong&gt; If an animation to open the main menu takes 2 seconds, then the test needs to wait 2 seconds. Multiply this by the number of tests that have to open the main menu.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Brittle:&lt;/strong&gt; What if the animation takes a little longer to run because the test computer is slow? We aren't trying to test performance here, yet the test will fail. What if the volume slider position changes depending on screen size? Eventually, you find yourself bending over backwards trying to stabilize the testing environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hard to maintain:&lt;/strong&gt; Suppose a designer decides to change the main menu animation to 3 seconds. Now all the tests that depend on the main menu need to be updated to wait for 3 seconds instead of 2.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clever developers will try to circumvent these problems by adding features that make the tests faster and more reliably. For example, instead of waiting 2 seconds for an animation to play, we can add a special &lt;code&gt;open-main-menu-completed&lt;/code&gt; event to the game that the test can wait for. Then, crank up the time scale to 10x speed so that the all animations complete in 0.2 seconds.&lt;/p&gt;

&lt;p&gt;While the intentions here are good, the fallacy is that now developers are spending more time debugging tests and writing test infrastructure instead of improving the product. In one of my past projects, we actually spent &lt;em&gt;more&lt;/em&gt; time debugging issues with the tests than we did with the product.&lt;/p&gt;

&lt;h4&gt;
  
  
  Ok, ok, but should I EVER write end-to-end tests?
&lt;/h4&gt;

&lt;p&gt;Probably not.&lt;/p&gt;

&lt;p&gt;Instead, design your app so that the parts that are difficult to get right can be tested in isolation. Leave the end-to-end testing to the humans. Or sentient AI, as the case may be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Tests in Unity using NUnit
&lt;/h2&gt;

&lt;p&gt;Unity has a built-in unit testing framework called &lt;a href="https://docs.unity3d.com/Packages/com.unity.test-framework@1.1/manual/index.html" rel="noopener noreferrer"&gt;NUnit&lt;/a&gt;. To get started, you just need to install the NUnit package from the Unity Package Manager.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Window &amp;gt; Package Manager.&lt;/li&gt;
&lt;li&gt;In the Package Manager window, select Unity Registry from the dropdown.&lt;/li&gt;
&lt;li&gt;Search for NUnit and click Install on the NUnit package.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Edit Mode vs Play Mode Tests
&lt;/h3&gt;

&lt;p&gt;In Unity, there are two type sof tests: edit mode tests and play mode tests.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edit Mode Tests:&lt;/strong&gt; These tests run within the Unity Editor. They are ideal for both unit tests and integration tests. Wherever possible, you should use edit mode tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Play Mode Tests:&lt;/strong&gt; These tests can run in the Unit Editor in play mode or in a standalone player. These types of tests should be used as a last resort since they are slower and less reliable. Whenever possible, it is better to update your components to support edit mode tests than to write play mode tests.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Edit Mode Tests
&lt;/h2&gt;

&lt;p&gt;For demonstration, we'll use this simple &lt;code&gt;SimpleCircleLayout&lt;/code&gt; component that arranges its children in a circle around its center.&lt;/p&gt;

&lt;p&gt;This is a super simplified version of the &lt;a href="https://www.flexalon.com/docs/circleLayout?utm_source=devto" rel="noopener noreferrer"&gt;FlexalonCircleLayout&lt;/a&gt; component, which has tests that are similar to the ones we'll write here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimpleCircleLayout&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Radius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LayoutChildren&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;angle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transform&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Radius&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Radius&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;angle&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PI&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;childCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a Assembly Definition for Edit Mode Tests
&lt;/h3&gt;

&lt;p&gt;Before we can write tests, we need to create a special assembly where our tests will live.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Window &amp;gt; General &amp;gt; Test Runner.&lt;/li&gt;
&lt;li&gt;If you don't have any tests in your project you'll see this:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbe0rkk7jobl389r7h5mb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbe0rkk7jobl389r7h5mb.png" alt="Test Runner - Create Edit Mode Tests" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click Create EditMode Test Assembly Folder. This will create a new "Tests" folder with an assembly definition file. You can rename this however you like.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In order to test your code, it needs to be in its own &lt;a href="https://docs.unity3d.com/6000.0/Documentation/Manual/assembly-definitions-intro.html" rel="noopener noreferrer"&gt;assembly definition file&lt;/a&gt;. If your script is in your main project, right click the Assets folder in the Project window and select Create &amp;gt; Assembly Definition.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxc8twdgl5j7ki3ua5mck.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxc8twdgl5j7ki3ua5mck.png" alt="Create Assembly Definition" width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Drag your assembly definition into the references of the test assembly definition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the final test assembly definition for Flexalon:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzvljghl73vce1m43pq9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzvljghl73vce1m43pq9c.png" alt="Flexalon Edit Mode Test Assembly" width="594" height="802"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Write your Test Class
&lt;/h3&gt;

&lt;p&gt;Now that we have our assembly set up, we can start writing tests. Create a new file called &lt;code&gt;SimpleCircleLayoutTests.cs&lt;/code&gt; in your test folder, next to the assembly definition file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TestFixture&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimpleCircleLayoutTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TestLayoutChildren&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SimpleCircleLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Radius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;child1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;child2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;child3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LayoutChildren&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;child1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1.73f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;child2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1.73f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;child3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this first test, we create a &lt;code&gt;SimpleCircleLayout&lt;/code&gt; component and add three child objects to it. We then call the &lt;code&gt;LayoutChildren&lt;/code&gt; method and assert that the children are positioned correctly in a circle around the center. Finally, we use the &lt;code&gt;Assert&lt;/code&gt; class to check if the positions are as expected.&lt;/p&gt;

&lt;p&gt;You can add other tests by creating new methods with the &lt;code&gt;[Test]&lt;/code&gt; attribute. Each test method should be self-contained and test a specific aspect of the component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Edit Mode Tests
&lt;/h3&gt;

&lt;p&gt;To run your edit mode tests, go to the Unity Test Runner window (Window &amp;gt; General &amp;gt; Test Runner) and click the Run All button. The test results will be displayed in the Test Runner window, showing you which tests passed and which failed.&lt;/p&gt;

&lt;p&gt;You'll notice, oddly, that the tests fail with the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TestLayoutChildren (0.012s)
---
Expected: (-1.00, 1.73, 0.00)
  But was:  (-1.00, 1.73, 0.00)
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But why?&lt;/p&gt;

&lt;h3&gt;
  
  
  Improving equality tests for Vector3 and Quaternion
&lt;/h3&gt;

&lt;p&gt;Unity's &lt;code&gt;Vector3&lt;/code&gt; and &lt;code&gt;Quaternion&lt;/code&gt; types are floating-point values, which can lead to precision issues. The actual Y value is not &lt;em&gt;exactly&lt;/em&gt; 1.73, Unity is just printing out the first 2 digits after the decimal.&lt;/p&gt;

&lt;p&gt;To address this, you can use a helper class to checks if the difference between two values is within a certain tolerance (0.01).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine.TestTools.Utils&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestUtil&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Vector3EqualityComparer&lt;/span&gt; &lt;span class="n"&gt;vector3Comparer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.01f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;QuaternionEqualityComparer&lt;/span&gt; &lt;span class="n"&gt;quaternionComparer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.01f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector3&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector3Comparer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Vectors are Equal"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AssertQuaternionEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quaternion&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quaternionComparer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Quaternions are Equal"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if we replace the &lt;code&gt;Assert.AreEqual&lt;/code&gt; calls with &lt;code&gt;TestUtil.AssertVector3Equal&lt;/code&gt;, the tests will pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frra7ywgzyfvqzdkp9vdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frra7ywgzyfvqzdkp9vdw.png" alt="Flexalon Edit Mode Test Run" width="619" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup and Teardown
&lt;/h3&gt;

&lt;p&gt;In some cases, you may find it convenient to perform common setup and teardown steps before and after each test. You can use the &lt;code&gt;[SetUp]&lt;/code&gt; and &lt;code&gt;[TearDown]&lt;/code&gt; attributes to mark methods that run before and after each test, respectively.&lt;/p&gt;

&lt;p&gt;For example, suppose we want to test the circle layout with different numbers of children.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TestFixture&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimpleCircleLayoutTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SimpleCircleLayout&lt;/span&gt; &lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SetUp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_layout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SimpleCircleLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Radius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TearDown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Teardown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DestroyImmediate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gameObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TestOneChild&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LayoutChildren&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;TestTwoChildren&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;child2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;child1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;child2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetParent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;_layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LayoutChildren&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;child1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertVector3Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;child2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the &lt;code&gt;Setup&lt;/code&gt; method creates a new &lt;code&gt;SimpleCircleLayout&lt;/code&gt; component before each test, and the &lt;code&gt;Teardown&lt;/code&gt; method destroys it after each test. This ensures that each test starts with a clean slate and doesn't interfere with other tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Attributes
&lt;/h3&gt;

&lt;p&gt;Unity's test framework provides several other attributes that you can use to organize and manage your tests effectively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[TestFixture]&lt;/code&gt;: Indicates a class that contains tests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[Test]&lt;/code&gt;: Marks a method as a test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[SetUp]&lt;/code&gt;: Identifies a method that runs before each test. It's used to set up conditions required for the tests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[TearDown]&lt;/code&gt;: Identifies a method that runs after each test. It's used to clean up any resources or state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[OneTimeSetUp]&lt;/code&gt;: Runs once before all tests in the test class.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[OneTimeTearDown]&lt;/code&gt;: Runs once after all tests in the test class.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[UnityTest]&lt;/code&gt;: For play mode tests (read on below). This tells Unity to run the test as a coroutine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generally speaking, be wary of &lt;code&gt;[OneTimeSetUp]&lt;/code&gt; and &lt;code&gt;[OneTimeTearDown]&lt;/code&gt;, as they can lead to dependencies between tests. For example, suppose we created the &lt;code&gt;SimpleCircleLayout&lt;/code&gt; in &lt;code&gt;[OneTimeSetUp]&lt;/code&gt; instead of &lt;code&gt;[SetUp]&lt;/code&gt;. The same layout would be shared between all tests. If one test adds children and doesn't clean up after itself, the next test would run with extra children, leading to unexpected failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play Mode Tests
&lt;/h2&gt;

&lt;p&gt;Play mode tests can run in the Unity Editor in play mode or in a standalone player. This can make play mode tests slow and brittle.&lt;/p&gt;

&lt;p&gt;But there are some scenarios where play mode tests makes sense because the behavior you want to takes multiple frames to complete.&lt;/p&gt;

&lt;p&gt;For example, Flexalon has a &lt;code&gt;FlexalonInteractable&lt;/code&gt; component that allows the player to click and drag an object. We need to test that if the player clicks on the interactable and drags it out of a layout, then the object ends up in the correct position and the layout updates correctly. Here's a real example:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;video controls="" loop=""&amp;gt;
    &amp;lt;source src="/images/blog/flexalon-grab-vr.mp4" type="video/mp4"&amp;gt;
&amp;lt;/source&amp;gt;&amp;lt;/video&amp;gt;
&amp;lt;p&amp;gt;Testing the VR grab interactables in Flexalon 3D Layouts&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;DraggableRemove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dragTarget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateDragTarget&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;interactable1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dragTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;interactable2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateInteractable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dragTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;MoveFromTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AreEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dragTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;childCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interactable1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dragTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indeed, these tests are the slowest and most brittle tests in Flexalon. In particular, the &lt;code&gt;MoveFromTo&lt;/code&gt; helper function took some quite some time to get right. But we feel these tests are worth the tradeoff because they test a critical part of the product, and there are many possible edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example that Requires Play Mode Tests
&lt;/h3&gt;

&lt;p&gt;To demonstrate play mode tests, let's add a &lt;code&gt;RotateOnce&lt;/code&gt; component. The component will rotate a gameObject once and only once around the Z-axis over a duration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RotateOnce&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_startTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnEnable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_startTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_startTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Euler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mathf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a Assembly Definition for Play Mode Tests
&lt;/h3&gt;

&lt;p&gt;Play mode tests need to be in a separate assembly from edit mode tests, since they cannot reference the UnityEditor namespace. Follow the same steps as for edit mode tests, but this time click "Play Mode".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3blvkgnk20w9zebrp3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg3blvkgnk20w9zebrp3h.png" alt="Test Runner - Create Play Mode Tests" width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Play Mode Test Basics
&lt;/h3&gt;

&lt;p&gt;Now that we have our assembly set up, we can start writing play mode tests. Create a new file called &lt;code&gt;RotateOnceTests.cs&lt;/code&gt; in your test folder, next to the assembly definition file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NUnit.Framework&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;UnityEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TestFixture&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RotateOnceTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UnityTest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IEnumerator&lt;/span&gt; &lt;span class="nf"&gt;TestRotateOnce&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rotateOnce&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GameObject&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RotateOnce&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;rotateOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertQuaternionEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Euler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rotateOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertQuaternionEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Euler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;240&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rotateOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertQuaternionEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Euler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rotateOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaitForSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Make sure it only rotates once!&lt;/span&gt;
        &lt;span class="n"&gt;TestUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertQuaternionEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Quaternion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Euler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rotateOnce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this test, we create a &lt;code&gt;RotateOnce&lt;/code&gt; component that will rotate once over 3 seconds, and then check the rotation at different points in time.&lt;/p&gt;

&lt;p&gt;For this test to work, we need to use the &lt;code&gt;[UnityTest]&lt;/code&gt; attribute instead of &lt;code&gt;[Test]&lt;/code&gt; and change the return type to &lt;code&gt;IEnumerator&lt;/code&gt;. This treats the method as a coroutine, and you can use &lt;code&gt;yield return&lt;/code&gt; to perform asynchronous operations. &lt;a href="https://docs.unity3d.com/6000.0/Documentation/Manual/coroutines.html" rel="noopener noreferrer"&gt;Learn more about coroutines in Unity&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Play Mode Tests
&lt;/h3&gt;

&lt;p&gt;To run your play mode tests, go to the Unity Test Runner window (Window &amp;gt; General &amp;gt; Test Runner) and select PlayMode from the dropdown. Click the Run All button to run the tests. The test results will be displayed in the Test Runner window, showing you which tests passed and which failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ca558v6n89jpc8h518x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ca558v6n89jpc8h518x.png" alt="Flexalon Edit Mode Test Run" width="619" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: If you add &lt;code&gt;Debug.Log&lt;/code&gt; statements to your test, they'll appear in the test results. This can be helpful for debugging failing tests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Run Tests from the Command Line
&lt;/h2&gt;

&lt;p&gt;Running tests manually is fine for small projects, but as your project grows, you'll want to automate your tests to catch issues early and ensure consistent quality. Unity provides command line options to run tests, allowing you to integrate them into your build pipeline or continuous integration (CI) system.&lt;/p&gt;

&lt;p&gt;To run your tests from the command line, use the &lt;code&gt;-runTests&lt;/code&gt; argument followed by the &lt;code&gt;-testPlatform&lt;/code&gt; to specify where to run the tests (EditMode or PlayMode).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Unity.exe &lt;span class="nt"&gt;-batchmode&lt;/span&gt; &lt;span class="nt"&gt;-nographics&lt;/span&gt; &lt;span class="nt"&gt;-runTests&lt;/span&gt; &lt;span class="nt"&gt;-projectPath&lt;/span&gt; &lt;span class="s2"&gt;"path/to/your/project"&lt;/span&gt; &lt;span class="nt"&gt;-testPlatform&lt;/span&gt; EditMode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: Do &lt;strong&gt;not&lt;/strong&gt; pass the &lt;code&gt;-quit&lt;/code&gt; flag to the command line, as it will quit Unity before any tests run. NUnit will automatically quit Unity after the tests are done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are additional options that you can use to filter which tests to run. See the &lt;a href="https://docs.unity3d.com/Packages/com.unity.test-framework@2.0/manual/reference-command-line.html" rel="noopener noreferrer"&gt;full list of command line arguments&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analyzing Test Results
&lt;/h3&gt;

&lt;p&gt;The command will generate a file named something like &lt;code&gt;TestResults-638683971208353675.xml&lt;/code&gt; in your project root directory, which you can inspect to see the results of your test. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;test-case&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"1305"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Collider2DFixedAndComponent"&lt;/span&gt; &lt;span class="na"&gt;fullname=&lt;/span&gt;&lt;span class="s"&gt;"FlexalonAdapterTests.Collider2DFixedAndComponent"&lt;/span&gt; &lt;span class="na"&gt;methodname=&lt;/span&gt;&lt;span class="s"&gt;"Collider2DFixedAndComponent"&lt;/span&gt; &lt;span class="na"&gt;classname=&lt;/span&gt;&lt;span class="s"&gt;"FlexalonAdapterTests"&lt;/span&gt; &lt;span class="na"&gt;runstate=&lt;/span&gt;&lt;span class="s"&gt;"Runnable"&lt;/span&gt; &lt;span class="na"&gt;seed=&lt;/span&gt;&lt;span class="s"&gt;"4201476"&lt;/span&gt; &lt;span class="na"&gt;result=&lt;/span&gt;&lt;span class="s"&gt;"Passed"&lt;/span&gt; &lt;span class="na"&gt;start-time=&lt;/span&gt;&lt;span class="s"&gt;"2024-11-26 03:42:24Z"&lt;/span&gt; &lt;span class="na"&gt;end-time=&lt;/span&gt;&lt;span class="s"&gt;"2024-11-26 03:42:24Z"&lt;/span&gt; &lt;span class="na"&gt;duration=&lt;/span&gt;&lt;span class="s"&gt;"0.001536"&lt;/span&gt; &lt;span class="na"&gt;asserts=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we see the result of test &lt;code&gt;FlexalonAdapterTests.Collider2DFixedAndComponent&lt;/code&gt; with &lt;code&gt;result="Passed"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Tests in Automation using GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Now that we know how to run tests from the command line, you can easily add it to any continuous integrate (CI) pipeline that you have set up.&lt;/p&gt;

&lt;p&gt;If you're using GitHub actions, we wrote the article &lt;a href="https://www.virtualmaker.dev/blog/automating-unity-builds-with-github-actions/?utm_source=devto" rel="noopener noreferrer"&gt;Automating Unity Builds with GitHub Actions&lt;/a&gt; that will help you get started.&lt;/p&gt;

&lt;p&gt;You can then update your workflow file to run the tests. We recommend injecting this after the &lt;code&gt;Project Validation&lt;/code&gt; step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Edit Mode Tests&lt;/span&gt;
&lt;span class="na"&gt;using&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-action@v1&lt;/span&gt;
  &lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.build-target }}&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-runTests -batchmode -testPlatform EditMode -testResults "${{ env.UNITY_PROJECT_PATH }}/Logs/EditMode-test-results.xml"&lt;/span&gt;
    &lt;span class="na"&gt;log-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EditMode-Tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need to update the actions/upload-artifacts step to add the test results to the uploaded artifacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;${{ github.workspace }}/**/*.xml # &amp;lt;- ADD THIS LINE&lt;/span&gt;
      &lt;span class="s"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The Unity NUnit package provides a powerful way to write tests to prevent bugs in your project. We covered the differences between unit testing, integration testing, and end-to-end testing, and when it's appropriate to use each.&lt;/p&gt;

&lt;p&gt;We also learned how to write edit mode tests and play mode tests, and how to run them from the command line or in automation using GitHub Actions.&lt;/p&gt;

&lt;p&gt;Armed with this knowledge, &lt;strong&gt;I expect bug free code from you from now on&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Go forth and test!&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.virtualmaker.dev?utm_source=devto" rel="noopener noreferrer"&gt;Original Article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.virtualmaker.dev?utm_source=devto" rel="noopener noreferrer"&gt;Virtual Maker Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.flexalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Flexalon 3D Layouts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>unittest</category>
      <category>gamedev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Automating Unity Builds with GitHub Actions</title>
      <dc:creator>Alon Farchy</dc:creator>
      <pubDate>Thu, 09 Jan 2025 15:40:15 +0000</pubDate>
      <link>https://dev.to/virtualmaker/automating-unity-builds-with-github-actions-1inf</link>
      <guid>https://dev.to/virtualmaker/automating-unity-builds-with-github-actions-1inf</guid>
      <description>&lt;p&gt;When working on a Unity project, you may find yourself making many builds to play test your changes. Since Unity locks the editor while building, this can leave you twiddling your thumbs for what seems like hours. By automating your builds, you can get your your time back, and at the same time streamline the development process for your team.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through how to use &lt;strong&gt;GitHub Actions&lt;/strong&gt; to automate Unity builds. We’ll learn step by step how to create a GitHub workflow, add the necessary secrets, and choose an appropriate build machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Build Automation (CI/CD)?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Continuous Integration (CI)&lt;/strong&gt; and &lt;strong&gt;Continuous Deployment (CD)&lt;/strong&gt; are practices that automate the process of building, testing, and deploying your project. CI/CD pipelines can be set up to run automatically whenever you push changes to your repository, ensuring you have a build ready to test at any time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; is a feature of GitHub that allows you run automated steps when changes happen in your GitHub repository. You can create custom workflows that run on specific triggers, such as pushing code to a repository or opening a pull request. These workflows can be used to build, test, and deploy your code automatically.&lt;/p&gt;

&lt;p&gt;If you're new to Git and GitHub, learn how to setup Git for Unity in our &lt;a href="https://www.virtualmaker.dev/blog/git-and-unity-a-comprehensive-guide-to-version-control-for-game-devs" rel="noopener noreferrer"&gt;Comprehensive Guide to Version Control for Game Devs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is &lt;span&gt;Buildalon&lt;/span&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.buildalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Buildalon&lt;/a&gt; turns GitHub Actions into a powerful CI/CD tool for Unity developers. It provides a set of free open-source GitHub Actions that we'll need to build a Unity project, and (optionally) build machines that support fast incremental builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa2pljr2k9dftujiz1dj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa2pljr2k9dftujiz1dj.png" alt="Buildalon Components" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Buildalon Quick Start (optional)
&lt;/h2&gt;

&lt;p&gt;In the rest of this article we'll cover the nitty-gritty details of writing GitHub Actions workflow to automate Unity builds.&lt;/p&gt;

&lt;p&gt;However, you can also use the &lt;a href="https://www.buildalon.com/start?utm_source=devto" rel="noopener noreferrer"&gt;Buildalon Quick Start&lt;/a&gt; to generate a workflow. This is the fastest way to get started, and you can come back to learn the details later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyebcgdc313qck1tgyacc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyebcgdc313qck1tgyacc.png" alt="Buildalon Quickstart" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Your Build Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create a Workflow File
&lt;/h3&gt;

&lt;p&gt;To get started, you need to create a workflow file in your repository. This file will define the steps that GitHub Actions will take to build your Unity project. Create the file &lt;code&gt;unity-build.yml&lt;/code&gt; and put in a directory named &lt;code&gt;.github/workflows&lt;/code&gt; in your repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add Triggers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Triggers&lt;/strong&gt; are events that tell GitHub to start a workflow. Triggers help streamline development by automatically initiating builds, tests, or deployments whenever key events occur, reducing the need for manual intervention.&lt;/p&gt;

&lt;p&gt;These example triggers will run the workflow on every push to the main branch and on every pull request to any branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Learn more about the different &lt;a href="https://www.buildalon.com/docs/workflows/triggers?utm_source=devto" rel="noopener noreferrer"&gt;triggers&lt;/a&gt; available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add a job
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;job&lt;/strong&gt; is a set of steps that run sequentially on the same build machine. Each job can run on a different machine, allowing you to parallelize your workflow and speed up the build process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Define the 'build' Job&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;UNITY_PROJECT_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.os }}&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;buildalon-windows&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon-windows&lt;/span&gt;
            &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StandaloneWindows64&lt;/span&gt;
            &lt;span class="na"&gt;build-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 'env' section defines environment variables that can be used in the job, and we'll need to declare the &lt;code&gt;UNITY_PROJECT_PATH&lt;/code&gt; variable for later steps.&lt;/p&gt;

&lt;p&gt;The 'runs-on' section specifies which build machines should be used, based on a runner label. Here we use a matrix strategy to define different operating systems and build targets. You can add more build targets in the future and run them in in parallel.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;buildalon-windows&lt;/code&gt; label tells GitHub to run this workflow on a Buildalon Windows runner. You can also use &lt;code&gt;buildalon-macos&lt;/code&gt; or &lt;code&gt;buildalon-ubuntu&lt;/code&gt; for macOS and Linux builds, respectively. Buildalon runners support incremental builds, which will speed up your builds considerably.&lt;/p&gt;

&lt;p&gt;If you prefer to use GitHub runners, change the label to &lt;code&gt;windows-latest&lt;/code&gt;, &lt;code&gt;macos-latest&lt;/code&gt;, or &lt;code&gt;ubuntu-latest&lt;/code&gt;. For advanced users, you can also set up your own &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners" rel="noopener noreferrer"&gt;self-hosted runners&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;StandaloneWindows64&lt;/code&gt; build target specifies that we want to build a 64-bit Windows standalone application. You can add more build targets by including them in the matrix, and these should match the names of the available &lt;a href="https://docs.unity3d.com/ScriptReference/BuildTarget.html" rel="noopener noreferrer"&gt;Unity build targets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can use &lt;code&gt;build-args&lt;/code&gt; to pass additional arguments to Unity when it is run from the command line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Add Steps
&lt;/h3&gt;

&lt;p&gt;The minimum steps we need to build a Unity project are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checkout the repository - &lt;a href="https://www.github.com/actions/checkout" rel="noopener noreferrer"&gt;actions/checkout&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install Unity - &lt;a href="https://www.buildalon.com/docs/actions/unity-setup?utm_source=devto" rel="noopener noreferrer"&gt;buildalon/unity-setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Activate the Unity license - &lt;a href="https://www.buildalon.com/docs/actions/activate-unity-license?utm_source=devto" rel="noopener noreferrer"&gt;buildalon/activate-unity-license&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Add the Buildalon Unity Plugin - &lt;a href="https://www.buildalon.com/docs/unity-plugin?utm_source=devto" rel="noopener noreferrer"&gt;com.virtualmaker.buildalon&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Build the project - &lt;a href="https://www.buildalon.com/docs/actions/unity-action?utm_source=devto" rel="noopener noreferrer"&gt;buildalon/unity-action&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Upload the build artifacts - &lt;a href="https://www.github.com/actions/upload-artifact" rel="noopener noreferrer"&gt;actions/upload-artifact&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is an example set of steps for making a Windows desktop build. These steps were generated using &lt;a href="https://www.buildalon.com/start?utm_source=devto" rel="noopener noreferrer"&gt;Buildalon Quick Start&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Since the set of steps is a bit different for each platform, we recommend using &lt;a href="https://www.buildalon.com/start" rel="noopener noreferrer"&gt;Buildalon Quick Start&lt;/a&gt; to generate the steps for your specific platform. For example, an additional step — &lt;a href="https://www.buildalon.com/docs/actions/unity-xcode-builder?utm_source=devto" rel="noopener noreferrer"&gt;buildalon/unity-xcode-builder&lt;/a&gt; — is required to run XCode to build an iOS project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;clean&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.clean == 'true' }}&lt;/span&gt;
          &lt;span class="na"&gt;lfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;submodules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recursive'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-setup@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;build-targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/activate-unity-license@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Personal'&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_USERNAME&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_PASSWORD&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add Build Pipeline Package&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;npm install -g openupm-cli&lt;/span&gt;
          &lt;span class="s"&gt;openupm add com.virtualmaker.buildalon&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Project Validation&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;log-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;project-validation'&lt;/span&gt;
          &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-quit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-batchmode&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-executeMethod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/unity-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-Build'&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;log-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-Build'&lt;/span&gt;
          &lt;span class="na"&gt;build-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-quit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-batchmode&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-executeMethod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Buildalon.Editor.BuildPipeline.UnityPlayerBuildTools.StartCommandLineBuild${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-args&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v6&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upload-artifact&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Upload&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Artifacts'&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success() || failure()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;compression-level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;github.run_number&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;github.run_attempt&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.os&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.build-target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-Artifacts'&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ github.workspace }}/**/*.log&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}/Builds/StandaloneWindows64/**/*.exe&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}/Builds/StandaloneWindows64/**/*.dll&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}/Builds/StandaloneWindows64/**/*_Data&lt;/span&gt;
            &lt;span class="s"&gt;${{ env.UNITY_PROJECT_PATH }}/Builds/StandaloneWindows64/MonoBleedingEdge/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Clean Artifacts&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pwsh&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Clean Logs&lt;/span&gt;
          &lt;span class="s"&gt;Get-ChildItem -Path "${{ env.UNITY_PROJECT_PATH }}" -File -Filter "*.log" -Recurse | Remove-Item -Force&lt;/span&gt;

          &lt;span class="s"&gt;$artifacts = "${{ env.UNITY_PROJECT_PATH }}/Builds"&lt;/span&gt;
          &lt;span class="s"&gt;Write-Host "::debug::Build artifacts path: $artifacts"&lt;/span&gt;

          &lt;span class="s"&gt;if (Test-Path -Path $artifacts) {&lt;/span&gt;
            &lt;span class="s"&gt;try {&lt;/span&gt;
              &lt;span class="s"&gt;Remove-Item $artifacts -Recurse -Force&lt;/span&gt;
            &lt;span class="s"&gt;} catch {&lt;/span&gt;
              &lt;span class="s"&gt;Write-Warning "Failed to delete artifacts folder file: $_"&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;
          &lt;span class="s"&gt;} else {&lt;/span&gt;
            &lt;span class="s"&gt;Write-Host "::debug::Artifacts folder not found."&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Add Secrets
&lt;/h3&gt;

&lt;p&gt;To activate the Unity license, the build machine needs access to your Unity username and password. You should never hardcode these values in your workflow file, as they can be exposed to the public. Instead, you can use GitHub Secrets to securely store sensitive information.&lt;/p&gt;

&lt;p&gt;Go to your repository on GitHub, click on &lt;code&gt;Settings&lt;/code&gt; &amp;gt; &lt;code&gt;Secrets &amp;amp; Variables&lt;/code&gt; &amp;gt; &lt;code&gt;Actions&lt;/code&gt; and add the following secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UNITY_USERNAME&lt;/code&gt;: Your Unity username, usually your email.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNITY_PASSWORD&lt;/code&gt;: Your Unity password.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3admp3ofy814va24g7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3admp3ofy814va24g7s.png" alt="Locating GitHub Secrets" width="800" height="106"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8wfmeoj81ew86ut50di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8wfmeoj81ew86ut50di.png" alt="Adding GitHub Secrets" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  For Unity Pro:
&lt;/h4&gt;

&lt;p&gt;If you're using Unity Pro, you also need to add the &lt;code&gt;UNITY_SERIAL&lt;/code&gt; secret with your serial key. Then go back to your workflow file and update the &lt;code&gt;activate-unity-license&lt;/code&gt; step to use the secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildalon/activate-unity-license@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;license&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Professional'&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_USERNAME&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_PASSWORD&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
    &lt;span class="na"&gt;serial&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secrets.UNITY_SERIAL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Install the Buildalon GitHub App
&lt;/h3&gt;

&lt;p&gt;To use Buildalon runners, you need to install the Buildalon GitHub App on your repository. This app gives you access to Buildalon runners to run your workflows.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://github.com/apps/buildalon/" rel="noopener noreferrer"&gt;Install the Buildalon App&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Buildalon runners are optimized for Unity builds and enable incremental builds, which will speed up your build times considerably.&lt;/p&gt;

&lt;p&gt;If you prefer to use GitHub's runners, you can skip this step, but make sure you update the runner label in your workflow file (for example, &lt;code&gt;windows-latest&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Push Your Changes to Start a Build
&lt;/h3&gt;

&lt;p&gt;Once you've added the workflow file and secrets to your repository, push your changes to GitHub. If your trigger conditions are met, GitHub Actions will automatically start the workflow and build your Unity project. You can find the status of the workflow in the &lt;code&gt;Actions&lt;/code&gt; tab of your repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Monitor Your Build
&lt;/h3&gt;

&lt;p&gt;Once the workflow is running, you can monitor its progress in the &lt;code&gt;Actions&lt;/code&gt; tab of your repository. You can view the logs of each step to see what's happening during the build process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmxel5zgj83k1zbyj7di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmxel5zgj83k1zbyj7di.png" alt="Click on GitHub Actions Tab" width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faiabbk0edanv5y3bzer6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faiabbk0edanv5y3bzer6.png" alt="View GitHub Run Logs" width="800" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If there are errors, you can usually find them in the &lt;code&gt;Summary&lt;/code&gt; page. Fix the errors in your project and push the changes to trigger a new build.&lt;/p&gt;

&lt;p&gt;For more help with common errors, take a look at the &lt;a href="https://www.buildalon.com/docs/troubleshooting?utm_source=devto" rel="noopener noreferrer"&gt;Buildalon troubleshooting page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9: Retrieve Your Build Artifacts
&lt;/h3&gt;

&lt;p&gt;Once the build is complete, you can download the build artifacts from the bottom of the &lt;code&gt;Summary&lt;/code&gt; page. These artifacts will contain the build files generated by Unity, such as the executable and data files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgb1s8oe959qsyr8iyw1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgb1s8oe959qsyr8iyw1o.png" alt="Download Artifacts from the Summary Page" width="800" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;🎉 Congratulations, you now have automated builds! 🎉&lt;/p&gt;

&lt;p&gt;Here are some ideas on how to augment your new workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add more build targets to the matrix to build for different platforms.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.buildalon.com/docs/workflows/test?utm_source=devto" rel="noopener noreferrer"&gt;Run unit tests automatically&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.buildalon.com/docs/workflows/deploy?utm_source=devto" rel="noopener noreferrer"&gt;Deploy your builds to an app store&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/webhooks/about-webhooks" rel="noopener noreferrer"&gt;Add webhooks&lt;/a&gt; to notify your team when a build is complete.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule" rel="noopener noreferrer"&gt;Create a branch protection policy&lt;/a&gt; to ensure that all code changes are tested before merging.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.virtualmaker.dev/blog/automating-unity-builds-with-github-actions?utm_source=devto" rel="noopener noreferrer"&gt;Original Article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.virtualmaker.dev?utm_source=devto" rel="noopener noreferrer"&gt;Virtual Maker Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.buildalon.com?utm_source=devto" rel="noopener noreferrer"&gt;Buildalon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.virtualmaker.dev/products?utm_source=devto" rel="noopener noreferrer"&gt;More Unity Tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>devops</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
