Table heading too low if on the top of the page

There are two problems.

\abovecaptionskip, \belowcaptionskip

The standard classes always assume that the caption goes below the object. Thus the book class sets a space of 10pt above the caption and 0pt below the caption.

In your case the table captions go above the tables, thus the values of \abovecaptionskip and \belowcaptionskip should be exchanged. For example, package caption does this for you:

\documentclass{article}
\usepackage{lipsum}
\usepackage{booktabs}
\usepackage{caption}
\usepackage[pass,showframe]{geometry}

\begin{document}

\lipsum[1]

\clearpage

\begin{table}[htbp]%
    \centering%
    \caption{Test table.}%
    \begin{tabular}{lcc}%
    \toprule
          a & b & c \\
    \bottomrule
    \end{tabular}%
\end{table}%

\end{document}

\topskip

TeX/LaTeX tries to align the first lines of the pages. TeX knows a skip register \topskip. This space is decreased by the height of the first line or set to zero, if the result would be negative. Then the resulting glue is added at the top of the page. The LaTeX default for \topskip is the font size (10pt, 11pt, or 12pt).

In case of the top float we have a box with a large height, thus the inserted \topskip is zero. However, the first page contains a normal line with height 6.94444pt. \topskip is 10pt, thus 3.05556pt is inserted.

The following patch \topcaptionfix tries to compensate this:

  1. With the help of package zref-savepos the current vertical position is recorded in the .aux file as label. For a top placed float the .aux file writes the label:

    \zref@newlabel{zref@1}{\posy{43889459}}
    

    Thus the vertical position is 43889459sp. Thus the fix does not apply, if the vertical position of the float object is smaller/lower.

  2. The height of the first line needs to be estimated. The code assumes that the normal font is used for the caption containing letters not larger than uppercase letters (AZ). A different text can be specified by the optional argument. The default is Table. If different fonts/sizes are used for the caption, then they need to be specified ne line \settoheight.

  3. Package caption also adds \struts. They are invisible objects occupying the vertical space of \baselineskip. Sometimes it helps for the alignment of caption lines. This feature can be configured by option strut, e.g. it can be turned off by \captionsetup{strut=off}.

  4. Finally the estimated height of the first line, the height of the \strut, if set and the value of \topskip are compared to calculate the space that needs to be inserted to correct the position of the float caption.

Example file:

% \showboxbreadth=\maxdimen
% \showboxdepth=\maxdimen

\documentclass{article}
\usepackage{lipsum}
\usepackage{booktabs}
\usepackage{caption}
\usepackage{zref-savepos}
\usepackage[pass,showframe]{geometry}

\makeatletter
\zref@require@unique
\providecommand*{\zref@unique@next}{%
  \stepcounter{zref@unique}%
}
\providecommand*{\zsaveposy}{\zsavepos}
\newcommand*{\topcaptionfix}[1][Table]{%
  \zref@unique@next
  \zsaveposy{\thezref@unique}%
  \zifrefundefined{\thezref@unique}{%
  }{%
    \ifdim\zposy{\thezref@unique}sp<43889459sp %
    \else
      \begingroup
        % estimated height of the first line of the caption
        \settoheight{\dimen@}{#1}%
        \caption@ifstrut{%
          \ifdim\dimen@>\ht\strutbox
            \ifdim\topskip>\dimen@
              \vskip\dimexpr\topskip-\dimen@\relax
            \fi
          \else
            \ifdim\topskip>\dimen@
              \vskip\dimexpr\topskip-\ht\strutbox\relax
            \fi
          \fi
        }{%
          \ifdim\topskip>\dimen@
            \vskip\dimexpr\topskip-\dimen@\relax
          \fi
        }%
      \endgroup
    \fi
  }%
}
\makeatother

\begin{document}

\lipsum[1]

% \showlists

\clearpage

\begin{table}[htbp]%
    \centering
    \topcaptionfix
    \caption{Test table.}%
    \begin{tabular}{lcc}% 
    \toprule
          a & b & c \\
    \bottomrule
    \end{tabular}%
\end{table}%

% \showlists

\end{document}

For those, who are familiar with TeX's box representations, can enable the commented lines \showbox... and \showlists. Then TeX prints the current vertical list into the .log file. For example, the start of the second page (variant with strut):

### current page:
\write-{}
\glue(\topskip) 0.0
\vbox(41.72083+0.0)x345.0
.\pdfsavepos
.\write1{\zref@newlabel{zref@1}{\posy{\Z@C@posy }}}
.\glue 1.60004
.\write1{\@writefile{lot}{\protect \contentsline {table}{\protect \numberline \ ETC.}
.\glue 0.0
.\glue(\parskip) 0.0
.\hbox(8.39996+3.60004)x345.0

The glue with value 1.60004pt is inserted to correct the position of the table caption.

Package geometry provides option showframe, that allows a visual control of the result:

Page 1:

page 1

Page 2:

page 2


Well it's a sort of feature...

The float box is set before it knows whether it will be placed at top of page and the float placement doesn't know what is inside the box when it places it at the top of the page.

It is possible to make the setting depend on position, but it goes deep into latex internal code in the output routine so probably it's incompatible with something. See

Formatting floats differently based on placement