How is the print time of an object to be printed estimated?
Much of the software used in 3D printing is open-source, and so are some slicers. Cura, for instance, does (or did, this source code is from an older branch) its print time estimation in gcodeInterpreter.py.

The relevant portion of the source code is (simplified and with many lines removed for clarity):

    totalMoveTimeMinute = 0.0
    pos = util3d.Vector3()

    for line in gcodeFile:
        G = self.getCodeInt(line, 'G')
        if G is not None:
            if G == 0 or G == 1:    #Move
                x = self.getCodeFloat(line, 'X')
                y = self.getCodeFloat(line, 'Y')
                z = self.getCodeFloat(line, 'Z')
                e = self.getCodeFloat(line, 'E')
                f = self.getCodeFloat(line, 'F')
                oldPos = pos.copy()
                pos.x = x
                pos.y = y
                pos.z = z
                feedrate = f
                currentE = e

                totalMoveTimeMinute += (oldPos - pos).vsize() / feedRate


As you can see, (this version of) Cura simply:


Loops over all the G-code instructions,
Computes the length of each move (in X/Y/Z) and divides that by the feedrate to get the time that move will take,
Sums this up over all the moves.


and does not take into account:


Acceleration or deceleration. It assumes the printer is always operating at the maximum feedrate,
The length of filament extruded. The feedrate is the speed for the move in (X,Y,Z,E), but Cura only looks at (X,Y,Z).
The time it takes to heat up the print bed/hotend or homing/autoleveling,
The effects of the printer slowing down if moves can not be read (from USB/SD-card) sufficiently fast (though this would be rather hard to include in any estimate).


The accuracy of this estimate can be arbitrarily bad if the feedrate is set to some unrealistic value.

Newer versions of Cura use a much more advanced time estimate method, and it can be found in timeEstimate.cpp.  It is much more complicated, and actually takes jerk/acceleration/deceleration into account. It is much more accurate.

We know exactly how 3D (open source) 3D printer firmwares work, so estimating print time is as easy as simulating execution of the G-code by your given firmware. There is no reason you can't get a really good estimation (if you take into account all of the intricacies of your given firmware's acceleration/deceleration techniques) but writing the code for it is rather involved.