diff --git a/__tests__/merge.test.ts b/__tests__/merge.test.ts
new file mode 100644
index 0000000..4b8c28d
--- /dev/null
+++ b/__tests__/merge.test.ts
@@ -0,0 +1,175 @@
+import * as core from '@actions/core'
+import artifact from '@actions/artifact'
+import {run} from '../src/merge/merge-artifact'
+import {Inputs} from '../src/merge/constants'
+import * as search from '../src/shared/search'
+
+const fixtures = {
+  artifactName: 'my-merged-artifact',
+  tmpDirectory: '/tmp/merge-artifact',
+  filesToUpload: [
+    '/some/artifact/path/file-a.txt',
+    '/some/artifact/path/file-b.txt',
+    '/some/artifact/path/file-c.txt'
+  ],
+  artifacts: [
+    {
+      name: 'my-artifact-a',
+      id: 1,
+      size: 100,
+      createdAt: new Date('2024-01-01T00:00:00Z')
+    },
+    {
+      name: 'my-artifact-b',
+      id: 2,
+      size: 100,
+      createdAt: new Date('2024-01-01T00:00:00Z')
+    },
+    {
+      name: 'my-artifact-c',
+      id: 3,
+      size: 100,
+      createdAt: new Date('2024-01-01T00:00:00Z')
+    }
+  ]
+}
+
+jest.mock('@actions/github', () => ({
+  context: {
+    repo: {
+      owner: 'actions',
+      repo: 'toolkit'
+    },
+    runId: 123,
+    serverUrl: 'https://github.com'
+  }
+}))
+
+jest.mock('@actions/core')
+
+jest.mock('fs/promises', () => ({
+  mkdtemp: jest.fn().mockResolvedValue('/tmp/merge-artifact'),
+  rm: jest.fn().mockResolvedValue(undefined)
+}))
+
+/* eslint-disable no-unused-vars */
+const mockInputs = (overrides?: Partial<{[K in Inputs]?: any}>) => {
+  const inputs = {
+    [Inputs.Into]: 'my-merged-artifact',
+    [Inputs.Pattern]: '*',
+    [Inputs.SeparateDirectories]: false,
+    [Inputs.RetentionDays]: 0,
+    [Inputs.CompressionLevel]: 6,
+    [Inputs.DeleteMerged]: false,
+    ...overrides
+  }
+
+  ;(core.getInput as jest.Mock).mockImplementation((name: string) => {
+    return inputs[name]
+  })
+  ;(core.getBooleanInput as jest.Mock).mockImplementation((name: string) => {
+    return inputs[name]
+  })
+
+  return inputs
+}
+
+describe('merge', () => {
+  beforeEach(async () => {
+    mockInputs()
+
+    jest
+      .spyOn(artifact, 'listArtifacts')
+      .mockResolvedValue({artifacts: fixtures.artifacts})
+
+    jest.spyOn(artifact, 'downloadArtifact').mockResolvedValue({
+      downloadPath: fixtures.tmpDirectory
+    })
+
+    jest.spyOn(search, 'findFilesToUpload').mockResolvedValue({
+      filesToUpload: fixtures.filesToUpload,
+      rootDirectory: fixtures.tmpDirectory
+    })
+
+    jest.spyOn(artifact, 'uploadArtifact').mockResolvedValue({
+      size: 123,
+      id: 1337
+    })
+
+    jest
+      .spyOn(artifact, 'deleteArtifact')
+      .mockImplementation(async artifactName => {
+        const artifact = fixtures.artifacts.find(a => a.name === artifactName)
+        if (!artifact) throw new Error(`Artifact ${artifactName} not found`)
+        return {id: artifact.id}
+      })
+  })
+
+  it('merges artifacts', async () => {
+    await run()
+
+    for (const a of fixtures.artifacts) {
+      expect(artifact.downloadArtifact).toHaveBeenCalledWith(a.id, {
+        path: fixtures.tmpDirectory
+      })
+    }
+
+    expect(artifact.uploadArtifact).toHaveBeenCalledWith(
+      fixtures.artifactName,
+      fixtures.filesToUpload,
+      fixtures.tmpDirectory,
+      {compressionLevel: 6}
+    )
+  })
+
+  it('fails if no artifacts found', async () => {
+    mockInputs({[Inputs.Pattern]: 'this-does-not-match'})
+
+    expect(run()).rejects.toThrow()
+
+    expect(artifact.uploadArtifact).not.toBeCalled()
+    expect(artifact.downloadArtifact).not.toBeCalled()
+  })
+
+  it('supports custom compression level', async () => {
+    mockInputs({
+      [Inputs.CompressionLevel]: 2
+    })
+
+    await run()
+
+    expect(artifact.uploadArtifact).toHaveBeenCalledWith(
+      fixtures.artifactName,
+      fixtures.filesToUpload,
+      fixtures.tmpDirectory,
+      {compressionLevel: 2}
+    )
+  })
+
+  it('supports custom retention days', async () => {
+    mockInputs({
+      [Inputs.RetentionDays]: 7
+    })
+
+    await run()
+
+    expect(artifact.uploadArtifact).toHaveBeenCalledWith(
+      fixtures.artifactName,
+      fixtures.filesToUpload,
+      fixtures.tmpDirectory,
+      {retentionDays: 7, compressionLevel: 6}
+    )
+  })
+
+  it('supports deleting artifacts after merge', async () => {
+    mockInputs({
+      [Inputs.DeleteMerged]: true
+    })
+
+    await run()
+
+    for (const a of fixtures.artifacts) {
+      expect(artifact.deleteArtifact).toHaveBeenCalledWith(a.name)
+    }
+  })
+})
diff --git a/src/merge/merge-artifact.ts b/src/merge/merge-artifact.ts
index 89bd295..81f4bf7 100644
--- a/src/merge/merge-artifact.ts
+++ b/src/merge/merge-artifact.ts
@@ -17,85 +17,77 @@ export const chunk = <T>(arr: T[], n: number): T[][] =>
   }, [] as T[][])
 
 export async function run(): Promise<void> {
+  const inputs = getInputs()
+  const tmpDir = await mkdtemp('merge-artifact')
+
+  const listArtifactResponse = await artifactClient.listArtifacts({
+    latest: true
+  })
+  const matcher = new Minimatch(inputs.pattern)
+  const artifacts = listArtifactResponse.artifacts.filter(artifact =>
+    matcher.match(artifact.name)
+  )
+  core.debug(
+    `Filtered from ${listArtifactResponse.artifacts.length} to ${artifacts.length} artifacts`
+  )
+
+  if (artifacts.length === 0) {
+    throw new Error(`No artifacts found matching pattern '${inputs.pattern}'`)
+  }
+
+  core.info(`Preparing to download the following artifacts:`)
+  artifacts.forEach(artifact => {
+    core.info(`- ${artifact.name} (ID: ${artifact.id}, Size: ${artifact.size})`)
+  })
+
+  const downloadPromises = artifacts.map(artifact =>
+    artifactClient.downloadArtifact(artifact.id, {
+      path: inputs.separateDirectories
+        ? path.join(tmpDir, artifact.name)
+        : tmpDir
+    })
+  )
+
+  const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
+  for (const chunk of chunkedPromises) {
+    await Promise.all(chunk)
+  }
+
+  const options: UploadArtifactOptions = {}
+  if (inputs.retentionDays) {
+    options.retentionDays = inputs.retentionDays
+  }
+
+  if (typeof inputs.compressionLevel !== 'undefined') {
+    options.compressionLevel = inputs.compressionLevel
+  }
+
+  const searchResult = await findFilesToUpload(tmpDir)
+
+  await uploadArtifact(
+    inputs.into,
+    searchResult.filesToUpload,
+    searchResult.rootDirectory,
+    options
+  )
+
+  core.info(
+    `The ${artifacts.length} artifact(s) have been successfully merged!`
+  )
+
+  if (inputs.deleteMerged) {
+    const deletePromises = artifacts.map(artifact =>
+      artifactClient.deleteArtifact(artifact.name)
+    )
+    await Promise.all(deletePromises)
+    core.info(`The ${artifacts.length} artifact(s) have been deleted`)
+  }
+
   try {
-    const inputs = getInputs()
-    const tmpDir = await mkdtemp('merge-artifact')
-
-    const listArtifactResponse = await artifactClient.listArtifacts({
-      latest: true
-    })
-    const matcher = new Minimatch(inputs.pattern)
-    const artifacts = listArtifactResponse.artifacts.filter(artifact =>
-      matcher.match(artifact.name)
-    )
-    core.debug(
-      `Filtered from ${listArtifactResponse.artifacts.length} to ${artifacts.length} artifacts`
-    )
-
-    if (artifacts.length === 0) {
-      throw new Error(`No artifacts found matching pattern '${inputs.pattern}'`)
-    }
-
-    core.info(`Preparing to download the following artifacts:`)
-    artifacts.forEach(artifact => {
-      core.info(
-        `- ${artifact.name} (ID: ${artifact.id}, Size: ${artifact.size})`
-      )
-    })
-
-    const downloadPromises = artifacts.map(artifact =>
-      artifactClient.downloadArtifact(artifact.id, {
-        path: inputs.separateDirectories
-          ? path.join(tmpDir, artifact.name)
-          : tmpDir
-      })
-    )
-
-    const chunkedPromises = chunk(downloadPromises, PARALLEL_DOWNLOADS)
-    for (const chunk of chunkedPromises) {
-      await Promise.all(chunk)
-    }
-
-    const options: UploadArtifactOptions = {}
-    if (inputs.retentionDays) {
-      options.retentionDays = inputs.retentionDays
-    }
-
-    if (typeof inputs.compressionLevel !== 'undefined') {
-      options.compressionLevel = inputs.compressionLevel
-    }
-
-    const searchResult = await findFilesToUpload(tmpDir)
-
-    await uploadArtifact(
-      inputs.into,
-      searchResult.filesToUpload,
-      searchResult.rootDirectory,
-      options
-    )
-
-    core.info(
-      `The ${artifacts.length} artifact(s) have been successfully merged!`
-    )
-
-    if (inputs.deleteMerged) {
-      const deletePromises = artifacts.map(artifact =>
-        artifactClient.deleteArtifact(artifact.name)
-      )
-      await Promise.all(deletePromises)
-      core.info(`The ${artifacts.length} artifact(s) have been deleted`)
-    }
-
-    try {
-      await rm(tmpDir, {recursive: true})
-    } catch (error) {
-      core.warning(
-        `Unable to remove temporary directory: ${(error as Error).message}`
-      )
-    }
+    await rm(tmpDir, {recursive: true})
   } catch (error) {
-    core.setFailed((error as Error).message)
+    core.warning(
+      `Unable to remove temporary directory: ${(error as Error).message}`
+    )
   }
 }
-
-run()