diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml
index 151fe250f2..bb662345cd 100644
--- a/models/fixtures/hook_task.yml
+++ b/models/fixtures/hook_task.yml
@@ -3,3 +3,4 @@
   repo_id: 1
   hook_id: 1
   uuid: uuid1
+  is_delivered: true
diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go
index bd5ea3fd38..b2b96fff9d 100644
--- a/modules/test/context_tests.go
+++ b/modules/test/context_tests.go
@@ -13,10 +13,11 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
 
+	"net/http/httptest"
+
 	"github.com/go-macaron/session"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/macaron.v1"
-	"net/http/httptest"
 )
 
 // MockContext mock context for unit tests
@@ -48,6 +49,16 @@ func LoadRepo(t *testing.T, ctx *context.Context, repoID int64) {
 	ctx.Repo.RepoLink = ctx.Repo.Repository.Link()
 }
 
+// LoadRepoCommit loads a repo's commit into a test context.
+func LoadRepoCommit(t *testing.T, ctx *context.Context) {
+	gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
+	assert.NoError(t, err)
+	branch, err := gitRepo.GetHEADBranch()
+	assert.NoError(t, err)
+	ctx.Repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
+	assert.NoError(t, err)
+}
+
 // LoadUser load a user into a test context.
 func LoadUser(t *testing.T, ctx *context.Context, userID int64) {
 	ctx.User = models.AssertExistsAndLoadBean(t, &models.User{ID: userID}).(*models.User)
diff --git a/public/swagger.v1.json b/public/swagger.v1.json
index dec618988b..86bf20a9a8 100644
--- a/public/swagger.v1.json
+++ b/public/swagger.v1.json
@@ -1565,6 +1565,46 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/hooks/{id}/tests": {
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Test a push webhook",
+        "operationId": "repoTestHook",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "integer",
+            "description": "id of the hook to test",
+            "name": "id",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/issue/{index}/comments": {
       "get": {
         "produces": [
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 02606bdfd0..eec55cac67 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -382,9 +382,12 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Group("/hooks", func() {
 					m.Combo("").Get(repo.ListHooks).
 						Post(bind(api.CreateHookOption{}), repo.CreateHook)
-					m.Combo("/:id").Get(repo.GetHook).
-						Patch(bind(api.EditHookOption{}), repo.EditHook).
-						Delete(repo.DeleteHook)
+					m.Group("/:id", func() {
+						m.Combo("").Get(repo.GetHook).
+							Patch(bind(api.EditHookOption{}), repo.EditHook).
+							Delete(repo.DeleteHook)
+						m.Post("/tests", context.RepoRef(), repo.TestHook)
+					})
 				}, reqToken(), reqRepoWriter())
 				m.Group("/collaborators", func() {
 					m.Get("", repo.ListCollaborators)
diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go
index 9c39094bae..e412a7f1f2 100644
--- a/routers/api/v1/repo/hook.go
+++ b/routers/api/v1/repo/hook.go
@@ -5,11 +5,11 @@
 package repo
 
 import (
+	"code.gitea.io/git"
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/convert"
 	"code.gitea.io/gitea/routers/api/v1/utils"
-
 	api "code.gitea.io/sdk/gitea"
 )
 
@@ -82,6 +82,62 @@ func GetHook(ctx *context.APIContext) {
 	ctx.JSON(200, convert.ToHook(repo.RepoLink, hook))
 }
 
+// TestHook tests a hook
+func TestHook(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{owner}/{repo}/hooks/{id}/tests repository repoTestHook
+	// ---
+	// summary: Test a push webhook
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: id
+	//   in: path
+	//   description: id of the hook to test
+	//   type: integer
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	if ctx.Repo.Commit == nil {
+		// if repo does not have any commits, then don't send a webhook
+		ctx.Status(204)
+		return
+	}
+
+	hookID := ctx.ParamsInt64(":id")
+	hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID)
+	if err != nil {
+		return
+	}
+
+	if err := models.PrepareWebhook(hook, ctx.Repo.Repository, models.HookEventPush, &api.PushPayload{
+		Ref:    git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
+		Before: ctx.Repo.Commit.ID.String(),
+		After:  ctx.Repo.Commit.ID.String(),
+		Commits: []*api.PayloadCommit{
+			convert.ToCommit(ctx.Repo.Repository, ctx.Repo.Commit),
+		},
+		Repo:   ctx.Repo.Repository.APIFormat(models.AccessModeNone),
+		Pusher: ctx.User.APIFormat(),
+		Sender: ctx.User.APIFormat(),
+	}); err != nil {
+		ctx.Error(500, "PrepareWebhook: ", err)
+		return
+	}
+	go models.HookQueue.Add(ctx.Repo.Repository.ID)
+	ctx.Status(204)
+}
+
 // CreateHook create a hook for a repository
 func CreateHook(ctx *context.APIContext, form api.CreateHookOption) {
 	// swagger:operation POST /repos/{owner}/{repo}/hooks repository repoCreateHook
diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go
new file mode 100644
index 0000000000..8ed4bc4b0c
--- /dev/null
+++ b/routers/api/v1/repo/hook_test.go
@@ -0,0 +1,33 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTestHook(t *testing.T) {
+	models.PrepareTestEnv(t)
+
+	ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
+	ctx.SetParams(":id", "1")
+	test.LoadRepo(t, ctx, 1)
+	test.LoadRepoCommit(t, ctx)
+	test.LoadUser(t, ctx, 2)
+	TestHook(&context.APIContext{Context: ctx, Org: nil})
+	assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status())
+
+	models.AssertExistsAndLoadBean(t, &models.HookTask{
+		RepoID: 1,
+		HookID: 1,
+	}, models.Cond("is_delivered=?", false))
+}
diff --git a/routers/api/v1/repo/main_test.go b/routers/api/v1/repo/main_test.go
new file mode 100644
index 0000000000..656758ffba
--- /dev/null
+++ b/routers/api/v1/repo/main_test.go
@@ -0,0 +1,16 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"path/filepath"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+)
+
+func TestMain(m *testing.M) {
+	models.MainTest(m, filepath.Join("..", "..", "..", ".."))
+}