Vueのテストとは、コンポーネントやcomposableを動かして「表示・イベント・状態の変化」が期待どおりかを自動で確かめることです。 標準的な組み合わせは、実行役(テストランナー)に Vitest、コンポーネントを表示して操作する道具に Vue Test Utils(@vue/test-utils) です。

この記事では、Vitest 5.0.2(2026年9月)+ Vue Test Utils 2.5.1 + Vue 3.5.43 で、よく使うテストをコピペで動くコードにし、合格時と失敗時の実際の出力を載せました。jsdom・happy-dom・ブラウザモード(本物のChromium)で同じテストを実行した速度比較、happy-domで msw が効かなかった実例も載せています。

Vueのテストの書き方 早見表

やりたいこと 書き方
コンポーネントを表示する const wrapper = mount(Comp, { props: {...} })
要素を探す(なければ失敗) wrapper.get('[data-test="title"]')
クリック・送信する await wrapper.get('button').trigger('click')
入力する await wrapper.get('input').setValue('文字')
emitを確認する expect(wrapper.emitted('add')[0]).toEqual(['A'])
slotを渡す mount(Comp, { slots: { extra: '<small>…</small>' } })
v-modelを確認する 'onUpdate:modelValue' で setProps する(後述)
APIなどのPromiseを待つ await flushPromises()
プラグインを入れる mount(Comp, { global: { plugins: [pinia, router] } })
Piniaの初期状態を差し込む createTestingPinia({ createSpy: vi.fn, initialState })(@pinia/testing)
ルーターを使う createRouter({ history: createMemoryHistory(), routes })
モジュールをモック vi.mock('../src/api/tasks', () => ({ fetchTasks: vi.fn() }))
通信をモック setupServer(http.get(url, () => HttpResponse.json([...])))(msw)

迷ったら「mount で表示 → get で探す → await trigger/setValue で操作 → expect で確認」の4手順で書けます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vueのテストの書き方|Vitest 5+Vue Test Utils実行結果つき」

最小構成(コピペで動く)

npm create vite@latest my-app -- --template vue
cd my-app
npm i -D vitest @vue/test-utils happy-dom

検証したバージョン:vitest 5.0.2、@vue/test-utils 2.5.1、vue 3.5.43、vite 8.3.1、@vitejs/plugin-vue 6.0.9、happy-dom 20.14.5、jsdom 30.1.1、msw 3.0.0、pinia 4.0.3、@pinia/testing 2.0.1、vue-router 5.3.1、Node.js 22.22.2。

Vitest 5 は Vite 6.4以上・Node.js 22.12以上が必要です。設定は vite.config.js の test に書けて、@vitejs/plugin-vue がそのままテストにも使われます。

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'happy-dom',   // 'jsdom' でも可
    include: ['tests/**/*.spec.js'],
  },
})

environment は「ブラウザの代わりになるDOM」の指定です(選び方は後半で実測)。実行は npx vitest(監視モード)か npx vitest run(1回だけ)です。

props・emit・slot のテスト

テスト対象(チェックで toggle、削除で remove を送る):

<!-- src/components/TaskItem.vue -->
<script setup>
const props = defineProps({
  title: { type: String, required: true },
  done: { type: Boolean, default: false },
})
const emit = defineEmits(['toggle', 'remove'])
</script>

<template>
  <li :class="{ done: props.done }" data-test="task">
    <label>
      <input type="checkbox" :checked="props.done" @change="emit('toggle')" />
      <span data-test="title">{{ props.title }}</span>
    </label>
    <slot name="extra" />
    <button data-test="remove" @click="emit('remove')">削除</button>
  </li>
</template>
// tests/TaskItem.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import TaskItem from '../src/components/TaskItem.vue'

describe('TaskItem', () => {
  it('props のタイトルを表示する', () => {
    const wrapper = mount(TaskItem, { props: { title: '備品を発注する' } })
    expect(wrapper.get('[data-test="title"]').text()).toBe('備品を発注する')
    expect(wrapper.classes()).not.toContain('done')
  })

  it('チェックで toggle、削除ボタンで remove を emit する', async () => {
    const wrapper = mount(TaskItem, { props: { title: 'A' } })
    await wrapper.get('input[type="checkbox"]').trigger('change')
    await wrapper.get('[data-test="remove"]').trigger('click')
    expect(wrapper.emitted('toggle')).toHaveLength(1)
    expect(wrapper.emitted()).toHaveProperty('remove')
  })

  it('名前付きスロット extra を表示する', () => {
    const wrapper = mount(TaskItem, {
      props: { title: 'A' },
      slots: { extra: '<small data-test="due">期限: 10/1</small>' },
    })
    expect(wrapper.get('[data-test="due"]').text()).toBe('期限: 10/1')
  })

  it('HTML のスナップショット', () => {
    const wrapper = mount(TaskItem, { props: { title: '会議室を予約する', done: true } })
    expect(wrapper.html()).toMatchSnapshot()
  })
})
$ npx vitest run tests/TaskItem.spec.js
 Test Files  1 passed (1)
      Tests  4 passed (4)

要素は data-test 属性で探すと、デザイン変更に強いテストになります。 get は見つからないと失敗し、find は exists() で有無を調べます。

失敗したときの出力

削除ボタンを「delete を送る」「文言は『消去』」に書き換えて実行した結果です。

[Vue warn]: Component emitted event "delete" but it is neither declared in the emits option nor as an "onDelete" prop.

 FAIL  tests/TaskItem.spec.js > TaskItem > チェックで toggle、削除ボタンで remove を emit する
AssertionError: expected { toggle: [ [] ], …(3) } to have property "remove"
 ❯ tests/TaskItem.spec.js:23:31

 FAIL  tests/TaskItem.spec.js > TaskItem > HTML のスナップショット
Error: Snapshot `TaskItem > HTML のスナップショット 1` mismatched
Expected: ""<li class="done" data-test="task">…<button data-test="remove">削除</button></li>""
Received: ""<li class="done" data-test="task">…<button data-test="remove">消去</button></li>""

  Snapshots  1 failed

失敗した行と「期待値」「実際の値」が並びます。 スナップショットの変更が意図どおりなら npx vitest run -u で更新します。

mount と shallowMount の違い

mount は子コンポーネントまで本物を表示し、shallowMount は子を中身のない「スタブ」に置き換えます。 一覧コンポーネント(入力フォームとタスク行を子に持つ)を shallowMount した実際のHTMLです。

<section>
  <task-form-stub modelvalue=""></task-form-stub>
  <ul>
    <task-item-stub title="棚卸し" done="false"></task-item-stub>
  </ul>
  <p data-test="remaining">残り 1 件</p>
</section>

スタブには子に渡したpropsが属性として残るので、「親が子に何を渡したか」を確かめられます。 迷ったら mount を基本にし、重い子だけ global: { stubs: { 子の名前: true } } でスタブにします。

v-model のテスト

v-model は「modelValue を受け取り update:modelValue を送る」仕組みなので、テストでは onUpdate:modelValue で setProps を呼んで親の役をします。

<!-- src/components/TaskForm.vue -->
<script setup>
import { ref } from 'vue'
const title = defineModel({ type: String, default: '' })
const emit = defineEmits(['add'])
const error = ref('')
function submit() {
  if (!title.value.trim()) { error.value = 'タイトルを入力してください'; return }
  error.value = ''
  emit('add', title.value.trim())
  title.value = ''
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model="title" data-test="input" />
    <button type="submit">追加</button>
    <p v-if="error" data-test="error">{{ error }}</p>
  </form>
</template>
// tests/TaskForm.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import TaskForm from '../src/components/TaskForm.vue'

describe('TaskForm(v-model)', () => {
  it('送信すると add を emit して入力を空にする', async () => {
    const wrapper = mount(TaskForm, {
      props: { modelValue: '', 'onUpdate:modelValue': (v) => wrapper.setProps({ modelValue: v }) },
    })
    await wrapper.get('[data-test="input"]').setValue('  在庫を数える  ')
    expect(wrapper.props('modelValue')).toBe('  在庫を数える  ')
    await wrapper.get('form').trigger('submit')
    expect(wrapper.emitted('add')[0]).toEqual(['在庫を数える'])
    expect(wrapper.props('modelValue')).toBe('')
  })
})

入力のルールは emitted の中身で、「空なら送らない」は expect(wrapper.emitted('add')).toBeUndefined() で確かめます。

非同期処理のテスト(flushPromises と nextTick)

trigger / setValue 後の再描画はその呼び出しに await、自分でrefを変えた後は await nextTick()、onMounted 内のAPI取得などPromiseの完了は await flushPromises()、タイマーは vi.useFakeTimers() で待ちます。

テスト対象(表示時にAPIから取得):

<!-- src/components/RemoteTasks.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import { fetchTasks } from '../api/tasks'
const tasks = ref([])
const state = ref('loading')
onMounted(async () => {
  try { tasks.value = await fetchTasks(); state.value = 'done' }
  catch (e) { state.value = 'error' }
})
</script>

<template>
  <p v-if="state === 'loading'" data-test="loading">読み込み中…</p>
  <p v-else-if="state === 'error'" data-test="error">取得に失敗しました</p>
  <ul v-else><li v-for="t in tasks" :key="t.id">{{ t.title }}</li></ul>
</template>
// src/api/tasks.js
export async function fetchTasks() {
  const res = await fetch('https://api.example.test/tasks')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}

方法1:vi.mock で API関数ごと差し替える

// tests/RemoteTasks.spec.js
import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import RemoteTasks from '../src/components/RemoteTasks.vue'
import { fetchTasks } from '../src/api/tasks'

vi.mock('../src/api/tasks', () => ({ fetchTasks: vi.fn() }))

describe('RemoteTasks(vi.mock)', () => {
  it('取得できたら一覧を表示する', async () => {
    vi.mocked(fetchTasks).mockResolvedValue([{ id: 1, title: '備品を発注する' }])
    const wrapper = mount(RemoteTasks)
    expect(wrapper.find('[data-test="loading"]').exists()).toBe(true)
    await flushPromises()
    expect(wrapper.findAll('li').map(li => li.text())).toEqual(['備品を発注する'])
  })
})

失敗時は mockRejectedValue(...) にします。vi.mock はファイルの先頭に巻き上げられるので、importより後に書いても先に効きます。 Vitest 5では clearMocks が既定で有効になり、各テストの前に呼び出し履歴が消えます(公式ブログ)。

方法2:msw で通信そのものを差し替える

msw(Mock Service Worker)は fetch の通信を受け止めて、用意した応答を返すライブラリです。

// tests/RemoteTasks.msw.spec.js
// @vitest-environment jsdom
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import RemoteTasks from '../src/components/RemoteTasks.vue'

const server = setupServer(
  http.get('https://api.example.test/tasks', () =>
    HttpResponse.json([{ id: 1, title: '会議室を予約する' }, { id: 2, title: '議事録を共有する' }])),
)
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

describe('RemoteTasks(msw)', () => {
  it('本物の fetch を通して一覧を表示する', async () => {
    const wrapper = mount(RemoteTasks)
    await flushPromises()
    expect(wrapper.findAll('li')).toHaveLength(2)
  })
})

先頭の // @vitest-environment jsdom がポイントです。 この行なしで happy-dom のまま実行すると、次のように失敗しました。

[MSW] Warning: intercepted a request without a matching request handler:
  • OPTIONS https://api.example.test/tasks
Error: getaddrinfo ENOTFOUND api.example.test
AssertionError: expected [] to have a length of 2 but got +0

happy-domの fetch で事前確認(OPTIONS)と実際の名前解決が走っていました。 このファイルだけ jsdom にすると合格しました(Node.js標準の fetch をmswが受け止める)。表示の分岐だけ見るなら vi.mock、URLやステータスの扱いまで見るなら msw と使い分けます。

composable のテスト

onMounted などを使わない composable は普通の関数として呼べます。 使う場合は、Vue公式ドキュメントにもある withSetup 補助関数でコンポーネントの中で動かします。

// src/composables/useCountdown.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useCountdown(seconds) {
  const left = ref(seconds)
  let id
  onMounted(() => { id = setInterval(() => { if (left.value > 0) left.value-- }, 1000) })
  onUnmounted(() => clearInterval(id))
  return { left }
}
// tests/withSetup.js
import { createApp } from 'vue'
export function withSetup(composable) {
  let result
  const app = createApp({ setup() { result = composable(); return () => {} } })
  app.mount(document.createElement('div'))
  return [result, app]
}
// tests/composables.spec.js
import { it, expect, vi, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { useCountdown } from '../src/composables/useCountdown'
import { withSetup } from './withSetup'

afterEach(() => vi.useRealTimers())

it('useCountdown:onMounted を使うので withSetup の中で動かす', async () => {
  vi.useFakeTimers()
  const [{ left }, app] = withSetup(() => useCountdown(3))
  vi.advanceTimersByTime(2000)
  await nextTick()
  expect(left.value).toBe(1)
  app.unmount()  // onUnmounted で clearInterval される
  vi.advanceTimersByTime(5000)
  expect(left.value).toBe(1)
})

unmount 後に値が変わらないことで後片付けも確認できます。

Pinia を使うコンポーネントのテスト

本物のストアで通しで確かめるなら global.plugins: [createPinia()]、初期状態を差し込んでactionの呼び出しだけ見るなら createTestingPinia を使います。 ストア自体のテストは別記事「Piniaの使い方」で扱っています。

// tests/TaskList.spec.js
import { it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import TaskList from '../src/components/TaskList.vue'
import { useTaskStore } from '../src/stores/tasks'

it('初期状態を差し込み、action の呼び出しだけ確認する', async () => {
  const wrapper = mount(TaskList, {
    global: {
      plugins: [createTestingPinia({
        createSpy: vi.fn,
        initialState: { tasks: { items: [{ id: 1, title: '棚卸し', done: false }] } },
      })],
    },
  })
  const store = useTaskStore()
  await wrapper.get('[data-test="remove"]').trigger('click')
  expect(store.remove).toHaveBeenCalledWith(1)
  // action はスタブなので、実際には削除されない
  expect(wrapper.findAll('[data-test="task"]')).toHaveLength(1)
})

createTestingPinia の既定では action は「呼ばれたことだけ記録するスタブ」で、中身は動きません。(TaskList.vue はストアの items を並べ、削除で store.remove(t.id) を呼ぶ一覧) 実際に動かしたいときは stubActions: false を指定します。@pinia/testing 2.0.1 は pinia >=4.0.2 が必要でした。

Vue Router を使うコンポーネントのテスト

テスト対象:

<!-- src/components/NavBack.vue -->
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
</script>

<template>
  <p data-test="path">{{ route.path }}</p>
  <RouterLink to="/tasks" data-test="link">タスク一覧へ</RouterLink>
  <button data-test="home" @click="router.push('/')">ホームへ</button>
</template>

方法1:本物のルーターをメモリ履歴で使う

createMemoryHistory() はURLを変えずメモリ上で履歴を持つモードで、テスト向きです。

// tests/NavBack.spec.js
import { it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import NavBack from '../src/components/NavBack.vue'

const routes = [
  { path: '/', component: { template: '<p>home</p>' } },
  { path: '/tasks', component: { template: '<p>tasks</p>' } },
]

it('本物のルーター(メモリ履歴)で画面遷移を確認する', async () => {
  const router = createRouter({ history: createMemoryHistory(), routes })
  router.push('/tasks')
  await router.isReady()
  const wrapper = mount(NavBack, { global: { plugins: [router] } })
  expect(wrapper.get('[data-test="path"]').text()).toBe('/tasks')
  await wrapper.get('[data-test="home"]').trigger('click')
  await flushPromises()
  expect(router.currentRoute.value.path).toBe('/')
  expect(wrapper.get('[data-test="path"]').text()).toBe('/')
})

方法2:vue-router をモックする

// tests/NavBack.mock.spec.js
import { it, expect, vi } from 'vitest'
import { mount, RouterLinkStub } from '@vue/test-utils'
import NavBack from '../src/components/NavBack.vue'

const push = vi.fn()
vi.mock('vue-router', () => ({
  useRoute: () => ({ path: '/tasks' }),
  useRouter: () => ({ push }),
}))

it('push の呼び出しと RouterLink の to だけ確認する', async () => {
  const wrapper = mount(NavBack, { global: { stubs: { RouterLink: RouterLinkStub } } })
  await wrapper.get('[data-test="home"]').trigger('click')
  expect(push).toHaveBeenCalledWith('/')
  expect(wrapper.getComponent(RouterLinkStub).props('to')).toBe('/tasks')
})

モックするテストはファイルを分け、先頭で vi.mock するのが確実です。 1ファイル内で vi.doMock と vi.resetModules で切り替えた版は、ブラウザモードで useRoute() が本物のまま動き Cannot read properties of undefined (reading 'path') で失敗しました。

jsdom・happy-dom・ブラウザモードの比較(速度を実測)

jsdom と happy-dom はNode.js上の疑似DOM、ブラウザモードは本物のChromiumで実行する方式です。 ブラウザモードでは msw/node などNode専用のものは使えません。

ブラウザモードの設定

npm i -D @vitest/browser-playwright playwright
npx playwright install chromium   # 今回は環境にあるChromiumを使ったので未実行
// vitest.browser.config.js
import { defineConfig, mergeConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import viteConfig from './vite.config.js'

export default mergeConfig(viteConfig, defineConfig({
  test: {
    exclude: ['tests/**/*.msw.spec.js'],   // msw/node はブラウザでは動かない
    api: { port: 7431, strictPort: true },  // ポートを固定したいとき(任意)
    browser: {
      enabled: true,
      headless: true,
      provider: playwright(),
      instances: [{ browser: 'chromium' }],
    },
  },
}))
$ npx vitest run -c vitest.browser.config.js
 Test Files  8 passed (8)
      Tests  18 passed (18)

同じ mount のテストが、そのまま本物のChromiumで合格しました。 ポートを browser.api に書くと「test.browser.api was deprecated in Vitest 5. Use test.api instead.」と表示されます。失敗時は .vitest/attachments/failure-screenshots/ にスクリーンショットが保存されました。

速度の実測結果

検証用の18件のテスト(8ファイル、mswを除く)を各方式で交互に5回実行し、npx vitest run の開始から終了までを測りました。

方式 中央値 範囲
jsdom(既定のプール) 17.0秒 12.5〜25.7秒
happy-dom(既定のプール) 12.3秒 7.7〜20.5秒
ブラウザモード(Chromium) 11.1秒 6.7〜15.3秒
happy-dom + --pool vmThreads 4.2秒 3.0〜6.2秒
jsdom + --pool vmThreads(3回) 3.0秒 2.9〜8.2秒

※2コアの共有環境で負荷が高く(ロードアベレージ約7)、ばらつきが大きいため、数値より「順位と桁」を参考にしてください。

  • 時間の大半は「DOM環境を作る時間」でした(内訳の environment がhappy-domで約60%、jsdomで約75%)
  • Vitest 5は実行後に 「happy-dom was created 8 times … create it once per worker with pool: 'vmThreads' … or isolate: false」 と改善案を表示します。vmThreads にすると3〜4秒台まで短くなりました(採用前に公式ドキュメントの注意点を確認)
  • ブラウザモードは、今回の規模では既定のjsdom・happy-domと同程度か速い結果でした

選び方の目安は、互換性重視なら jsdom、軽さ重視なら happy-dom、CSSや実際の入力イベントまで確かめたいならブラウザモードです。テストが増えて遅くなったら pool: 'vmThreads' や isolate: false を検討します。

スナップショットとカバレッジ

スナップショットは wrapper.html() を保存し、次回以降の変化を比べる方法です。 初回実行で tests/__snapshots__/TaskItem.spec.js.snap が作られました。小さな部品に絞ると差分が読みやすくなります。

カバレッジ(テストで実行されたコードの割合)は @vitest/coverage-v8 を入れて測ります。

npm i -D @vitest/coverage-v8
npx vitest run --coverage --coverage.include='src/**'
File              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
All files         |     100 |    92.59 |     100 |     100 |
  useCountdown.js |     100 |       50 |     100 |     100 | 5
  tasks.js        |     100 |       50 |     100 |     100 | 8

(検証時の出力から、フォルダ行と罫線を省略)

すべて100%のファイル(.vue など)は表に出ず、分岐(Branch)に漏れがあるファイルだけが表示されました。 5行目は「0になったら減らさない」分岐で、0まで数えるテストを足せば埋まります。カバレッジは「テストが足りない場所を探す地図」として使います。

よくある失敗と直し方

症状 原因 直し方
クリックしても表示が変わらない await がない await ...trigger('click')
APIの結果が表示されない Promiseの完了を待っていない await flushPromises()
injection "Symbol(router)" not found ルーターを渡していない global.plugins: [router] か vi.mock('vue-router')
mswの応答が返らない happy-domの fetch を通らない そのファイルを jsdom に
Vitest 5に上げたら非同期の expect で失敗 await し忘れのアサーションが失敗扱いに await expect(p).resolves... と書く

よくある質問

Q. JestからVitestに移る必要はありますか? A. Jestで安定して動いているなら、使い続ける判断にも十分な理由があります。Viteのプロジェクトなら、Vitestは vite.config を共有でき設定が少なく済みます。書き方はほぼ共通です。

Q. wrapper.vm で内部の変数を見てもよいですか? A. 動きますが壊れやすくなります。画面の文字や emitted を確かめる方が長持ちします。

Q. Vue 2のプロジェクトでもVue Test Utilsは使えますか? A. Vue 2にはVue Test Utils 1系が対応しています。長く現場を支えてきたVue 2のプロジェクトでも、mount / trigger / emitted の基本は同じです。

まとめ

  • Vueのテストは Vitest(実行役)+ Vue Test Utils(表示と操作) が標準。設定は vite.config.js の test に書ける
  • 基本は mount → get → await trigger/setValue → expect。shallowMount は子を xxx-stub に置き換える
  • 非同期は flushPromises、時間は vi.useFakeTimers、composable は withSetup で動かす
  • Piniaは createTestingPinia(actionは既定でスタブ)、ルーターは createMemoryHistory か、ファイルを分けた vi.mock
  • happy-domではmswが応答を返さなかった → そのファイルを jsdom にすると合格
  • 実測では環境作成が時間の大半。vmThreads で3〜4秒台に、ブラウザモードも既定の疑似DOMと同程度の速さだった

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Vueのテストの書き方|Vitest 5+Vue Test Utils実行結果つき」

株式会社RJCでは、テストを品質を守る仕組みとしてだけでなく、コードの意図を仲間に伝える手段としても大切にし、プロジェクトの中で書き方を共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?