- Vue.js
Vueのテストの書き方|Vitest 5+Vue Test Utils実行結果つき
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期連続の黒字決算。